Home | History | Annotate | Download | only in testharness
      1 /*global self*/
      2 /*jshint latedef: nofunc*/
      3 /*
      4 Distributed under both the W3C Test Suite License [1] and the W3C
      5 3-clause BSD License [2]. To contribute to a W3C Test Suite, see the
      6 policies and contribution forms [3].
      7 
      8 [1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license
      9 [2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license
     10 [3] http://www.w3.org/2004/10/27-testcases
     11 */
     12 
     13 /* Documentation is in docs/api.md */
     14 
     15 (function ()
     16 {
     17     var debug = false;
     18     // default timeout is 10 seconds, test can override if needed
     19     var settings = {
     20         output:true,
     21         harness_timeout:{
     22             "normal":10000,
     23             "long":60000
     24         },
     25         test_timeout:null
     26     };
     27 
     28     var xhtml_ns = "http://www.w3.org/1999/xhtml";
     29 
     30     // script_prefix is used by Output.prototype.show_results() to figure out
     31     // where to get testharness.css from.  It's enclosed in an extra closure to
     32     // not pollute the library's namespace with variables like "src".
     33     var script_prefix = null;
     34     (function ()
     35     {
     36         var scripts = document.getElementsByTagName("script");
     37         for (var i = 0; i < scripts.length; i++) {
     38             var src;
     39             if (scripts[i].src) {
     40                 src = scripts[i].src;
     41             } else if (scripts[i].href) {
     42                 //SVG case
     43                 src = scripts[i].href.baseVal;
     44             }
     45 
     46             if (src && src.slice(src.length - "testharness.js".length) === "testharness.js") {
     47                 script_prefix = src.slice(0, src.length - "testharness.js".length);
     48                 break;
     49             }
     50         }
     51     })();
     52 
     53     /*
     54      * API functions
     55      */
     56 
     57     var name_counter = 0;
     58     function next_default_name()
     59     {
     60         //Don't use document.title to work around an Opera bug in XHTML documents
     61         var title = document.getElementsByTagName("title")[0];
     62         var prefix = (title && title.firstChild && title.firstChild.data) || "Untitled";
     63         var suffix = name_counter > 0 ? " " + name_counter : "";
     64         name_counter++;
     65         return prefix + suffix;
     66     }
     67 
     68     function test(func, name, properties)
     69     {
     70         var test_name = name ? name : next_default_name();
     71         properties = properties ? properties : {};
     72         var test_obj = new Test(test_name, properties);
     73         test_obj.step(func, test_obj, test_obj);
     74         if (test_obj.phase === test_obj.phases.STARTED) {
     75             test_obj.done();
     76         }
     77     }
     78 
     79     function async_test(func, name, properties)
     80     {
     81         if (typeof func !== "function") {
     82             properties = name;
     83             name = func;
     84             func = null;
     85         }
     86         var test_name = name ? name : next_default_name();
     87         properties = properties ? properties : {};
     88         var test_obj = new Test(test_name, properties);
     89         if (func) {
     90             test_obj.step(func, test_obj, test_obj);
     91         }
     92         return test_obj;
     93     }
     94 
     95     function setup(func_or_properties, maybe_properties)
     96     {
     97         var func = null;
     98         var properties = {};
     99         if (arguments.length === 2) {
    100             func = func_or_properties;
    101             properties = maybe_properties;
    102         } else if (func_or_properties instanceof Function) {
    103             func = func_or_properties;
    104         } else {
    105             properties = func_or_properties;
    106         }
    107         tests.setup(func, properties);
    108         output.setup(properties);
    109     }
    110 
    111     function done() {
    112         if (tests.tests.length === 0) {
    113             tests.set_file_is_test();
    114         }
    115         if (tests.file_is_test) {
    116             tests.tests[0].done();
    117         }
    118         tests.end_wait();
    119     }
    120 
    121     function generate_tests(func, args, properties) {
    122         forEach(args, function(x, i)
    123                 {
    124                     var name = x[0];
    125                     test(function()
    126                          {
    127                              func.apply(this, x.slice(1));
    128                          },
    129                          name,
    130                          Array.isArray(properties) ? properties[i] : properties);
    131                 });
    132     }
    133 
    134     function on_event(object, event, callback)
    135     {
    136         object.addEventListener(event, callback, false);
    137     }
    138 
    139     expose(test, 'test');
    140     expose(async_test, 'async_test');
    141     expose(generate_tests, 'generate_tests');
    142     expose(setup, 'setup');
    143     expose(done, 'done');
    144     expose(on_event, 'on_event');
    145 
    146     /*
    147      * Return a string truncated to the given length, with ... added at the end
    148      * if it was longer.
    149      */
    150     function truncate(s, len)
    151     {
    152         if (s.length > len) {
    153             return s.substring(0, len - 3) + "...";
    154         }
    155         return s;
    156     }
    157 
    158     /*
    159      * Return true if object is probably a Node object.
    160      */
    161     function is_node(object)
    162     {
    163         // I use duck-typing instead of instanceof, because
    164         // instanceof doesn't work if the node is from another window (like an
    165         // iframe's contentWindow):
    166         // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295
    167         if ("nodeType" in object &&
    168             "nodeName" in object &&
    169             "nodeValue" in object &&
    170             "childNodes" in object) {
    171             try {
    172                 object.nodeType;
    173             } catch (e) {
    174                 // The object is probably Node.prototype or another prototype
    175                 // object that inherits from it, and not a Node instance.
    176                 return false;
    177             }
    178             return true;
    179         }
    180         return false;
    181     }
    182 
    183     /*
    184      * Convert a value to a nice, human-readable string
    185      */
    186     function format_value(val, seen)
    187     {
    188         if (!seen) {
    189             seen = [];
    190         }
    191         if (typeof val === "object" && val !== null) {
    192             if (seen.indexOf(val) >= 0) {
    193                 return "[...]";
    194             }
    195             seen.push(val);
    196         }
    197         if (Array.isArray(val)) {
    198             return "[" + val.map(function(x) {return format_value(x, seen);}).join(", ") + "]";
    199         }
    200 
    201         switch (typeof val) {
    202         case "string":
    203             val = val.replace("\\", "\\\\");
    204             for (var i = 0; i < 32; i++) {
    205                 var replace = "\\";
    206                 switch (i) {
    207                 case 0: replace += "0"; break;
    208                 case 1: replace += "x01"; break;
    209                 case 2: replace += "x02"; break;
    210                 case 3: replace += "x03"; break;
    211                 case 4: replace += "x04"; break;
    212                 case 5: replace += "x05"; break;
    213                 case 6: replace += "x06"; break;
    214                 case 7: replace += "x07"; break;
    215                 case 8: replace += "b"; break;
    216                 case 9: replace += "t"; break;
    217                 case 10: replace += "n"; break;
    218                 case 11: replace += "v"; break;
    219                 case 12: replace += "f"; break;
    220                 case 13: replace += "r"; break;
    221                 case 14: replace += "x0e"; break;
    222                 case 15: replace += "x0f"; break;
    223                 case 16: replace += "x10"; break;
    224                 case 17: replace += "x11"; break;
    225                 case 18: replace += "x12"; break;
    226                 case 19: replace += "x13"; break;
    227                 case 20: replace += "x14"; break;
    228                 case 21: replace += "x15"; break;
    229                 case 22: replace += "x16"; break;
    230                 case 23: replace += "x17"; break;
    231                 case 24: replace += "x18"; break;
    232                 case 25: replace += "x19"; break;
    233                 case 26: replace += "x1a"; break;
    234                 case 27: replace += "x1b"; break;
    235                 case 28: replace += "x1c"; break;
    236                 case 29: replace += "x1d"; break;
    237                 case 30: replace += "x1e"; break;
    238                 case 31: replace += "x1f"; break;
    239                 }
    240                 val = val.replace(RegExp(String.fromCharCode(i), "g"), replace);
    241             }
    242             return '"' + val.replace(/"/g, '\\"') + '"';
    243         case "boolean":
    244         case "undefined":
    245             return String(val);
    246         case "number":
    247             // In JavaScript, -0 === 0 and String(-0) == "0", so we have to
    248             // special-case.
    249             if (val === -0 && 1/val === -Infinity) {
    250                 return "-0";
    251             }
    252             return String(val);
    253         case "object":
    254             if (val === null) {
    255                 return "null";
    256             }
    257 
    258             // Special-case Node objects, since those come up a lot in my tests.  I
    259             // ignore namespaces.
    260             if (is_node(val)) {
    261                 switch (val.nodeType) {
    262                 case Node.ELEMENT_NODE:
    263                     var ret = "<" + val.localName;
    264                     for (var i = 0; i < val.attributes.length; i++) {
    265                         ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"';
    266                     }
    267                     ret += ">" + val.innerHTML + "</" + val.localName + ">";
    268                     return "Element node " + truncate(ret, 60);
    269                 case Node.TEXT_NODE:
    270                     return 'Text node "' + truncate(val.data, 60) + '"';
    271                 case Node.PROCESSING_INSTRUCTION_NODE:
    272                     return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60));
    273                 case Node.COMMENT_NODE:
    274                     return "Comment node <!--" + truncate(val.data, 60) + "-->";
    275                 case Node.DOCUMENT_NODE:
    276                     return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
    277                 case Node.DOCUMENT_TYPE_NODE:
    278                     return "DocumentType node";
    279                 case Node.DOCUMENT_FRAGMENT_NODE:
    280                     return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
    281                 default:
    282                     return "Node object of unknown type";
    283                 }
    284             }
    285 
    286         /* falls through */
    287         default:
    288             return typeof val + ' "' + truncate(String(val), 60) + '"';
    289         }
    290     }
    291     expose(format_value, "format_value");
    292 
    293     /*
    294      * Assertions
    295      */
    296 
    297     function assert_true(actual, description)
    298     {
    299         assert(actual === true, "assert_true", description,
    300                                 "expected true got ${actual}", {actual:actual});
    301     }
    302     expose(assert_true, "assert_true");
    303 
    304     function assert_false(actual, description)
    305     {
    306         assert(actual === false, "assert_false", description,
    307                                  "expected false got ${actual}", {actual:actual});
    308     }
    309     expose(assert_false, "assert_false");
    310 
    311     function same_value(x, y) {
    312         if (y !== y) {
    313             //NaN case
    314             return x !== x;
    315         }
    316         if (x === 0 && y === 0) {
    317             //Distinguish +0 and -0
    318             return 1/x === 1/y;
    319         }
    320         return x === y;
    321     }
    322 
    323     function assert_equals(actual, expected, description)
    324     {
    325          /*
    326           * Test if two primitives are equal or two objects
    327           * are the same object
    328           */
    329         if (typeof actual != typeof expected) {
    330             assert(false, "assert_equals", description,
    331                           "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}",
    332                           {expected:expected, actual:actual});
    333             return;
    334         }
    335         assert(same_value(actual, expected), "assert_equals", description,
    336                                              "expected ${expected} but got ${actual}",
    337                                              {expected:expected, actual:actual});
    338     }
    339     expose(assert_equals, "assert_equals");
    340 
    341     function assert_not_equals(actual, expected, description)
    342     {
    343          /*
    344           * Test if two primitives are unequal or two objects
    345           * are different objects
    346           */
    347         assert(!same_value(actual, expected), "assert_not_equals", description,
    348                                               "got disallowed value ${actual}",
    349                                               {actual:actual});
    350     }
    351     expose(assert_not_equals, "assert_not_equals");
    352 
    353     function assert_in_array(actual, expected, description)
    354     {
    355         assert(expected.indexOf(actual) != -1, "assert_in_array", description,
    356                                                "value ${actual} not in array ${expected}",
    357                                                {actual:actual, expected:expected});
    358     }
    359     expose(assert_in_array, "assert_in_array");
    360 
    361     function assert_object_equals(actual, expected, description)
    362     {
    363          //This needs to be improved a great deal
    364          function check_equal(actual, expected, stack)
    365          {
    366              stack.push(actual);
    367 
    368              var p;
    369              for (p in actual) {
    370                  assert(expected.hasOwnProperty(p), "assert_object_equals", description,
    371                                                     "unexpected property ${p}", {p:p});
    372 
    373                  if (typeof actual[p] === "object" && actual[p] !== null) {
    374                      if (stack.indexOf(actual[p]) === -1) {
    375                          check_equal(actual[p], expected[p], stack);
    376                      }
    377                  } else {
    378                      assert(same_value(actual[p], expected[p]), "assert_object_equals", description,
    379                                                        "property ${p} expected ${expected} got ${actual}",
    380                                                        {p:p, expected:expected, actual:actual});
    381                  }
    382              }
    383              for (p in expected) {
    384                  assert(actual.hasOwnProperty(p),
    385                         "assert_object_equals", description,
    386                         "expected property ${p} missing", {p:p});
    387              }
    388              stack.pop();
    389          }
    390          check_equal(actual, expected, []);
    391     }
    392     expose(assert_object_equals, "assert_object_equals");
    393 
    394     function assert_array_equals(actual, expected, description)
    395     {
    396         assert(actual.length === expected.length,
    397                "assert_array_equals", description,
    398                "lengths differ, expected ${expected} got ${actual}",
    399                {expected:expected.length, actual:actual.length});
    400 
    401         for (var i = 0; i < actual.length; i++) {
    402             assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
    403                    "assert_array_equals", description,
    404                    "property ${i}, property expected to be $expected but was $actual",
    405                    {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
    406                    actual:actual.hasOwnProperty(i) ? "present" : "missing"});
    407             assert(same_value(expected[i], actual[i]),
    408                    "assert_array_equals", description,
    409                    "property ${i}, expected ${expected} but got ${actual}",
    410                    {i:i, expected:expected[i], actual:actual[i]});
    411         }
    412     }
    413     expose(assert_array_equals, "assert_array_equals");
    414 
    415     function assert_approx_equals(actual, expected, epsilon, description)
    416     {
    417         /*
    418          * Test if two primitive numbers are equal withing +/- epsilon
    419          */
    420         assert(typeof actual === "number",
    421                "assert_approx_equals", description,
    422                "expected a number but got a ${type_actual}",
    423                {type_actual:typeof actual});
    424 
    425         assert(Math.abs(actual - expected) <= epsilon,
    426                "assert_approx_equals", description,
    427                "expected ${expected} +/- ${epsilon} but got ${actual}",
    428                {expected:expected, actual:actual, epsilon:epsilon});
    429     }
    430     expose(assert_approx_equals, "assert_approx_equals");
    431 
    432     function assert_less_than(actual, expected, description)
    433     {
    434         /*
    435          * Test if a primitive number is less than another
    436          */
    437         assert(typeof actual === "number",
    438                "assert_less_than", description,
    439                "expected a number but got a ${type_actual}",
    440                {type_actual:typeof actual});
    441 
    442         assert(actual < expected,
    443                "assert_less_than", description,
    444                "expected a number less than ${expected} but got ${actual}",
    445                {expected:expected, actual:actual});
    446     }
    447     expose(assert_less_than, "assert_less_than");
    448 
    449     function assert_greater_than(actual, expected, description)
    450     {
    451         /*
    452          * Test if a primitive number is greater than another
    453          */
    454         assert(typeof actual === "number",
    455                "assert_greater_than", description,
    456                "expected a number but got a ${type_actual}",
    457                {type_actual:typeof actual});
    458 
    459         assert(actual > expected,
    460                "assert_greater_than", description,
    461                "expected a number greater than ${expected} but got ${actual}",
    462                {expected:expected, actual:actual});
    463     }
    464     expose(assert_greater_than, "assert_greater_than");
    465 
    466     function assert_less_than_equal(actual, expected, description)
    467     {
    468         /*
    469          * Test if a primitive number is less than or equal to another
    470          */
    471         assert(typeof actual === "number",
    472                "assert_less_than_equal", description,
    473                "expected a number but got a ${type_actual}",
    474                {type_actual:typeof actual});
    475 
    476         assert(actual <= expected,
    477                "assert_less_than", description,
    478                "expected a number less than or equal to ${expected} but got ${actual}",
    479                {expected:expected, actual:actual});
    480     }
    481     expose(assert_less_than_equal, "assert_less_than_equal");
    482 
    483     function assert_greater_than_equal(actual, expected, description)
    484     {
    485         /*
    486          * Test if a primitive number is greater than or equal to another
    487          */
    488         assert(typeof actual === "number",
    489                "assert_greater_than_equal", description,
    490                "expected a number but got a ${type_actual}",
    491                {type_actual:typeof actual});
    492 
    493         assert(actual >= expected,
    494                "assert_greater_than_equal", description,
    495                "expected a number greater than or equal to ${expected} but got ${actual}",
    496                {expected:expected, actual:actual});
    497     }
    498     expose(assert_greater_than_equal, "assert_greater_than_equal");
    499 
    500     function assert_regexp_match(actual, expected, description) {
    501         /*
    502          * Test if a string (actual) matches a regexp (expected)
    503          */
    504         assert(expected.test(actual),
    505                "assert_regexp_match", description,
    506                "expected ${expected} but got ${actual}",
    507                {expected:expected, actual:actual});
    508     }
    509     expose(assert_regexp_match, "assert_regexp_match");
    510 
    511     function assert_class_string(object, class_string, description) {
    512         assert_equals({}.toString.call(object), "[object " + class_string + "]",
    513                       description);
    514     }
    515     expose(assert_class_string, "assert_class_string");
    516 
    517 
    518     function _assert_own_property(name) {
    519         return function(object, property_name, description)
    520         {
    521             assert(object.hasOwnProperty(property_name),
    522                    name, description,
    523                    "expected property ${p} missing", {p:property_name});
    524         };
    525     }
    526     expose(_assert_own_property("assert_exists"), "assert_exists");
    527     expose(_assert_own_property("assert_own_property"), "assert_own_property");
    528 
    529     function assert_not_exists(object, property_name, description)
    530     {
    531         assert(!object.hasOwnProperty(property_name),
    532                "assert_not_exists", description,
    533                "unexpected property ${p} found", {p:property_name});
    534     }
    535     expose(assert_not_exists, "assert_not_exists");
    536 
    537     function _assert_inherits(name) {
    538         return function (object, property_name, description)
    539         {
    540             assert(typeof object === "object",
    541                    name, description,
    542                    "provided value is not an object");
    543 
    544             assert("hasOwnProperty" in object,
    545                    name, description,
    546                    "provided value is an object but has no hasOwnProperty method");
    547 
    548             assert(!object.hasOwnProperty(property_name),
    549                    name, description,
    550                    "property ${p} found on object expected in prototype chain",
    551                    {p:property_name});
    552 
    553             assert(property_name in object,
    554                    name, description,
    555                    "property ${p} not found in prototype chain",
    556                    {p:property_name});
    557         };
    558     }
    559     expose(_assert_inherits("assert_inherits"), "assert_inherits");
    560     expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute");
    561 
    562     function assert_readonly(object, property_name, description)
    563     {
    564          var initial_value = object[property_name];
    565          try {
    566              //Note that this can have side effects in the case where
    567              //the property has PutForwards
    568              object[property_name] = initial_value + "a"; //XXX use some other value here?
    569              assert(same_value(object[property_name], initial_value),
    570                     "assert_readonly", description,
    571                     "changing property ${p} succeeded",
    572                     {p:property_name});
    573          } finally {
    574              object[property_name] = initial_value;
    575          }
    576     }
    577     expose(assert_readonly, "assert_readonly");
    578 
    579     function assert_throws(code, func, description)
    580     {
    581         try {
    582             func.call(this);
    583             assert(false, "assert_throws", description,
    584                    "${func} did not throw", {func:func});
    585         } catch (e) {
    586             if (e instanceof AssertionError) {
    587                 throw e;
    588             }
    589             if (code === null) {
    590                 return;
    591             }
    592             if (typeof code === "object") {
    593                 assert(typeof e == "object" && "name" in e && e.name == code.name,
    594                        "assert_throws", description,
    595                        "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})",
    596                                     {func:func, actual:e, actual_name:e.name,
    597                                      expected:code,
    598                                      expected_name:code.name});
    599                 return;
    600             }
    601 
    602             var code_name_map = {
    603                 INDEX_SIZE_ERR: 'IndexSizeError',
    604                 HIERARCHY_REQUEST_ERR: 'HierarchyRequestError',
    605                 WRONG_DOCUMENT_ERR: 'WrongDocumentError',
    606                 INVALID_CHARACTER_ERR: 'InvalidCharacterError',
    607                 NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError',
    608                 NOT_FOUND_ERR: 'NotFoundError',
    609                 NOT_SUPPORTED_ERR: 'NotSupportedError',
    610                 INVALID_STATE_ERR: 'InvalidStateError',
    611                 SYNTAX_ERR: 'SyntaxError',
    612                 INVALID_MODIFICATION_ERR: 'InvalidModificationError',
    613                 NAMESPACE_ERR: 'NamespaceError',
    614                 INVALID_ACCESS_ERR: 'InvalidAccessError',
    615                 TYPE_MISMATCH_ERR: 'TypeMismatchError',
    616                 SECURITY_ERR: 'SecurityError',
    617                 NETWORK_ERR: 'NetworkError',
    618                 ABORT_ERR: 'AbortError',
    619                 URL_MISMATCH_ERR: 'URLMismatchError',
    620                 QUOTA_EXCEEDED_ERR: 'QuotaExceededError',
    621                 TIMEOUT_ERR: 'TimeoutError',
    622                 INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError',
    623                 DATA_CLONE_ERR: 'DataCloneError'
    624             };
    625 
    626             var name = code in code_name_map ? code_name_map[code] : code;
    627 
    628             var name_code_map = {
    629                 IndexSizeError: 1,
    630                 HierarchyRequestError: 3,
    631                 WrongDocumentError: 4,
    632                 InvalidCharacterError: 5,
    633                 NoModificationAllowedError: 7,
    634                 NotFoundError: 8,
    635                 NotSupportedError: 9,
    636                 InvalidStateError: 11,
    637                 SyntaxError: 12,
    638                 InvalidModificationError: 13,
    639                 NamespaceError: 14,
    640                 InvalidAccessError: 15,
    641                 TypeMismatchError: 17,
    642                 SecurityError: 18,
    643                 NetworkError: 19,
    644                 AbortError: 20,
    645                 URLMismatchError: 21,
    646                 QuotaExceededError: 22,
    647                 TimeoutError: 23,
    648                 InvalidNodeTypeError: 24,
    649                 DataCloneError: 25,
    650 
    651                 UnknownError: 0,
    652                 ConstraintError: 0,
    653                 DataError: 0,
    654                 TransactionInactiveError: 0,
    655                 ReadOnlyError: 0,
    656                 VersionError: 0
    657             };
    658 
    659             if (!(name in name_code_map)) {
    660                 throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()');
    661             }
    662 
    663             var required_props = { code: name_code_map[name] };
    664 
    665             if (required_props.code === 0 ||
    666                ("name" in e && e.name !== e.name.toUpperCase() && e.name !== "DOMException")) {
    667                 // New style exception: also test the name property.
    668                 required_props.name = name;
    669             }
    670 
    671             //We'd like to test that e instanceof the appropriate interface,
    672             //but we can't, because we don't know what window it was created
    673             //in.  It might be an instanceof the appropriate interface on some
    674             //unknown other window.  TODO: Work around this somehow?
    675 
    676             assert(typeof e == "object",
    677                    "assert_throws", description,
    678                    "${func} threw ${e} with type ${type}, not an object",
    679                    {func:func, e:e, type:typeof e});
    680 
    681             for (var prop in required_props) {
    682                 assert(typeof e == "object" && prop in e && e[prop] == required_props[prop],
    683                        "assert_throws", description,
    684                        "${func} threw ${e} that is not a DOMException " + code + ": property ${prop} is equal to ${actual}, expected ${expected}",
    685                        {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]});
    686             }
    687         }
    688     }
    689     expose(assert_throws, "assert_throws");
    690 
    691     function assert_unreached(description) {
    692          assert(false, "assert_unreached", description,
    693                 "Reached unreachable code");
    694     }
    695     expose(assert_unreached, "assert_unreached");
    696 
    697     function assert_any(assert_func, actual, expected_array)
    698     {
    699         var args = [].slice.call(arguments, 3);
    700         var errors = [];
    701         var passed = false;
    702         forEach(expected_array,
    703                 function(expected)
    704                 {
    705                     try {
    706                         assert_func.apply(this, [actual, expected].concat(args));
    707                         passed = true;
    708                     } catch (e) {
    709                         errors.push(e.message);
    710                     }
    711                 });
    712         if (!passed) {
    713             throw new AssertionError(errors.join("\n\n"));
    714         }
    715     }
    716     expose(assert_any, "assert_any");
    717 
    718     function Test(name, properties)
    719     {
    720         if (tests.file_is_test && tests.tests.length) {
    721             throw new Error("Tried to create a test with file_is_test");
    722         }
    723         this.name = name;
    724 
    725         this.phases = {
    726             INITIAL:0,
    727             STARTED:1,
    728             HAS_RESULT:2,
    729             COMPLETE:3
    730         };
    731         this.phase = this.phases.INITIAL;
    732 
    733         this.status = this.NOTRUN;
    734         this.timeout_id = null;
    735 
    736         this.properties = properties;
    737         var timeout = properties.timeout ? properties.timeout : settings.test_timeout;
    738         if (timeout != null) {
    739             this.timeout_length = timeout * tests.timeout_multiplier;
    740         } else {
    741             this.timeout_length = null;
    742         }
    743 
    744         this.message = null;
    745 
    746         this.steps = [];
    747 
    748         this.cleanup_callbacks = [];
    749 
    750         tests.push(this);
    751     }
    752 
    753     Test.statuses = {
    754         PASS:0,
    755         FAIL:1,
    756         TIMEOUT:2,
    757         NOTRUN:3
    758     };
    759 
    760     Test.prototype = merge({}, Test.statuses);
    761 
    762     Test.prototype.structured_clone = function()
    763     {
    764         if (!this._structured_clone) {
    765             var msg = this.message;
    766             msg = msg ? String(msg) : msg;
    767             this._structured_clone = merge({
    768                 name:String(this.name),
    769                 status:this.status,
    770                 message:msg
    771             }, Test.statuses);
    772         }
    773         return this._structured_clone;
    774     };
    775 
    776     Test.prototype.step = function(func, this_obj)
    777     {
    778         if (this.phase > this.phases.STARTED) {
    779             return;
    780         }
    781         this.phase = this.phases.STARTED;
    782         //If we don't get a result before the harness times out that will be a test timout
    783         this.set_status(this.TIMEOUT, "Test timed out");
    784 
    785         tests.started = true;
    786 
    787         if (this.timeout_id === null) {
    788             this.set_timeout();
    789         }
    790 
    791         this.steps.push(func);
    792 
    793         if (arguments.length === 1) {
    794             this_obj = this;
    795         }
    796 
    797         try {
    798             return func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
    799         } catch (e) {
    800             if (this.phase >= this.phases.HAS_RESULT) {
    801                 return;
    802             }
    803             var message = (typeof e === "object" && e !== null) ? e.message : e;
    804             if (typeof e.stack != "undefined" && typeof e.message == "string") {
    805                 //Try to make it more informative for some exceptions, at least
    806                 //in Gecko and WebKit.  This results in a stack dump instead of
    807                 //just errors like "Cannot read property 'parentNode' of null"
    808                 //or "root is null".  Makes it a lot longer, of course.
    809                 message += "(stack: " + e.stack + ")";
    810             }
    811             this.set_status(this.FAIL, message);
    812             this.phase = this.phases.HAS_RESULT;
    813             this.done();
    814         }
    815     };
    816 
    817     Test.prototype.step_func = function(func, this_obj)
    818     {
    819         var test_this = this;
    820 
    821         if (arguments.length === 1) {
    822             this_obj = test_this;
    823         }
    824 
    825         return function()
    826         {
    827             return test_this.step.apply(test_this, [func, this_obj].concat(
    828                 Array.prototype.slice.call(arguments)));
    829         };
    830     };
    831 
    832     Test.prototype.step_func_done = function(func, this_obj)
    833     {
    834         var test_this = this;
    835 
    836         if (arguments.length === 1) {
    837             this_obj = test_this;
    838         }
    839 
    840         return function()
    841         {
    842             if (func) {
    843                 test_this.step.apply(test_this, [func, this_obj].concat(
    844                     Array.prototype.slice.call(arguments)));
    845             }
    846             test_this.done();
    847         };
    848     };
    849 
    850     Test.prototype.unreached_func = function(description)
    851     {
    852         return this.step_func(function() {
    853             assert_unreached(description);
    854         });
    855     };
    856 
    857     Test.prototype.add_cleanup = function(callback) {
    858         this.cleanup_callbacks.push(callback);
    859     };
    860 
    861     Test.prototype.force_timeout = function() {
    862         this.set_status(this.TIMEOUT);
    863         this.phase = this.phases.HAS_RESULT;
    864     }
    865 
    866     Test.prototype.set_timeout = function()
    867     {
    868         if (this.timeout_length !== null) {
    869             var this_obj = this;
    870             this.timeout_id = setTimeout(function()
    871                                          {
    872                                              this_obj.timeout();
    873                                          }, this.timeout_length);
    874         }
    875     };
    876 
    877     Test.prototype.set_status = function(status, message)
    878     {
    879         this.status = status;
    880         this.message = message;
    881     };
    882 
    883     Test.prototype.timeout = function()
    884     {
    885         this.timeout_id = null;
    886         this.set_status(this.TIMEOUT, "Test timed out");
    887         this.phase = this.phases.HAS_RESULT;
    888         this.done();
    889     };
    890 
    891     Test.prototype.done = function()
    892     {
    893         if (this.phase == this.phases.COMPLETE) {
    894             return;
    895         }
    896 
    897         if (this.phase <= this.phases.STARTED) {
    898             this.set_status(this.PASS, null);
    899         }
    900 
    901         if (this.status == this.NOTRUN) {
    902             alert(this.phase);
    903         }
    904 
    905         this.phase = this.phases.COMPLETE;
    906 
    907         clearTimeout(this.timeout_id);
    908         tests.result(this);
    909         this.cleanup();
    910     };
    911 
    912     Test.prototype.cleanup = function() {
    913         forEach(this.cleanup_callbacks,
    914                 function(cleanup_callback) {
    915                     cleanup_callback();
    916                 });
    917     };
    918 
    919     /*
    920      * Harness
    921      */
    922 
    923     function TestsStatus()
    924     {
    925         this.status = null;
    926         this.message = null;
    927     }
    928 
    929     TestsStatus.statuses = {
    930         OK:0,
    931         ERROR:1,
    932         TIMEOUT:2
    933     };
    934 
    935     TestsStatus.prototype = merge({}, TestsStatus.statuses);
    936 
    937     TestsStatus.prototype.structured_clone = function()
    938     {
    939         if (!this._structured_clone) {
    940             var msg = this.message;
    941             msg = msg ? String(msg) : msg;
    942             this._structured_clone = merge({
    943                 status:this.status,
    944                 message:msg
    945             }, TestsStatus.statuses);
    946         }
    947         return this._structured_clone;
    948     };
    949 
    950     function Tests()
    951     {
    952         this.tests = [];
    953         this.num_pending = 0;
    954 
    955         this.phases = {
    956             INITIAL:0,
    957             SETUP:1,
    958             HAVE_TESTS:2,
    959             HAVE_RESULTS:3,
    960             COMPLETE:4
    961         };
    962         this.phase = this.phases.INITIAL;
    963 
    964         this.properties = {};
    965 
    966         //All tests can't be done until the load event fires
    967         this.all_loaded = false;
    968         this.wait_for_finish = false;
    969         this.processing_callbacks = false;
    970 
    971         this.allow_uncaught_exception = false;
    972 
    973         this.file_is_test = false;
    974 
    975         this.timeout_multiplier = 1;
    976         this.timeout_length = this.get_timeout();
    977         this.timeout_id = null;
    978 
    979         this.start_callbacks = [];
    980         this.test_done_callbacks = [];
    981         this.all_done_callbacks = [];
    982 
    983         this.status = new TestsStatus();
    984 
    985         var this_obj = this;
    986 
    987         on_event(window, "load",
    988                  function()
    989                  {
    990                      this_obj.all_loaded = true;
    991                      if (this_obj.all_done())
    992                      {
    993                          this_obj.complete();
    994                      }
    995                  });
    996 
    997         this.set_timeout();
    998     }
    999 
   1000     Tests.prototype.setup = function(func, properties)
   1001     {
   1002         if (this.phase >= this.phases.HAVE_RESULTS) {
   1003             return;
   1004         }
   1005 
   1006         if (this.phase < this.phases.SETUP) {
   1007             this.phase = this.phases.SETUP;
   1008         }
   1009 
   1010         this.properties = properties;
   1011 
   1012         for (var p in properties) {
   1013             if (properties.hasOwnProperty(p)) {
   1014                 var value = properties[p];
   1015                 if (p == "allow_uncaught_exception") {
   1016                     this.allow_uncaught_exception = value;
   1017                 } else if (p == "explicit_done" && value) {
   1018                     this.wait_for_finish = true;
   1019                 } else if (p == "explicit_timeout" && value) {
   1020                     this.timeout_length = null;
   1021                     if (this.timeout_id)
   1022                     {
   1023                         clearTimeout(this.timeout_id);
   1024                     }
   1025                 } else if (p == "timeout_multiplier") {
   1026                     this.timeout_multiplier = value;
   1027                 }
   1028             }
   1029         }
   1030 
   1031         if (func) {
   1032             try {
   1033                 func();
   1034             } catch (e) {
   1035                 this.status.status = this.status.ERROR;
   1036                 this.status.message = String(e);
   1037             }
   1038         }
   1039         this.set_timeout();
   1040     };
   1041 
   1042     Tests.prototype.set_file_is_test = function() {
   1043         if (this.tests.length > 0) {
   1044             throw new Error("Tried to set file as test after creating a test");
   1045         }
   1046         this.wait_for_finish = true;
   1047         this.file_is_test = true;
   1048         // Create the test, which will add it to the list of tests
   1049         async_test();
   1050     };
   1051 
   1052     Tests.prototype.get_timeout = function() {
   1053         var metas = document.getElementsByTagName("meta");
   1054         for (var i = 0; i < metas.length; i++) {
   1055             if (metas[i].name == "timeout") {
   1056                 if (metas[i].content == "long") {
   1057                     return settings.harness_timeout.long;
   1058                 }
   1059                 break;
   1060             }
   1061         }
   1062         return settings.harness_timeout.normal;
   1063     };
   1064 
   1065     Tests.prototype.set_timeout = function() {
   1066         var this_obj = this;
   1067         clearTimeout(this.timeout_id);
   1068         if (this.timeout_length !== null) {
   1069             this.timeout_id = setTimeout(function() {
   1070                                              this_obj.timeout();
   1071                                          }, this.timeout_length);
   1072         }
   1073     };
   1074 
   1075     Tests.prototype.timeout = function() {
   1076         if (this.status.status === null) {
   1077             this.status.status = this.status.TIMEOUT;
   1078         }
   1079         this.complete();
   1080     };
   1081 
   1082     Tests.prototype.end_wait = function()
   1083     {
   1084         this.wait_for_finish = false;
   1085         if (this.all_done()) {
   1086             this.complete();
   1087         }
   1088     };
   1089 
   1090     Tests.prototype.push = function(test)
   1091     {
   1092         if (this.phase < this.phases.HAVE_TESTS) {
   1093             this.start();
   1094         }
   1095         this.num_pending++;
   1096         this.tests.push(test);
   1097     };
   1098 
   1099     Tests.prototype.all_done = function() {
   1100         return (this.tests.length > 0 && this.all_loaded && this.num_pending === 0 &&
   1101                 !this.wait_for_finish && !this.processing_callbacks);
   1102     };
   1103 
   1104     Tests.prototype.start = function() {
   1105         this.phase = this.phases.HAVE_TESTS;
   1106         this.notify_start();
   1107     };
   1108 
   1109     Tests.prototype.notify_start = function() {
   1110         var this_obj = this;
   1111         forEach (this.start_callbacks,
   1112                  function(callback)
   1113                  {
   1114                      callback(this_obj.properties);
   1115                  });
   1116         forEach_windows(
   1117                 function(w, is_same_origin)
   1118                 {
   1119                     if (is_same_origin && w.start_callback) {
   1120                         try {
   1121                             w.start_callback(this_obj.properties);
   1122                         } catch (e) {
   1123                             if (debug) {
   1124                                 throw e;
   1125                             }
   1126                         }
   1127                     }
   1128                     if (supports_post_message(w) && w !== self) {
   1129                         w.postMessage({
   1130                             type: "start",
   1131                             properties: this_obj.properties
   1132                         }, "*");
   1133                     }
   1134                 });
   1135     };
   1136 
   1137     Tests.prototype.result = function(test)
   1138     {
   1139         if (this.phase > this.phases.HAVE_RESULTS) {
   1140             return;
   1141         }
   1142         this.phase = this.phases.HAVE_RESULTS;
   1143         this.num_pending--;
   1144         this.notify_result(test);
   1145     };
   1146 
   1147     Tests.prototype.notify_result = function(test) {
   1148         var this_obj = this;
   1149         this.processing_callbacks = true;
   1150         forEach(this.test_done_callbacks,
   1151                 function(callback)
   1152                 {
   1153                     callback(test, this_obj);
   1154                 });
   1155 
   1156         forEach_windows(
   1157                 function(w, is_same_origin)
   1158                 {
   1159                     if (is_same_origin && w.result_callback) {
   1160                         try {
   1161                             w.result_callback(test);
   1162                         } catch (e) {
   1163                             if (debug) {
   1164                                 throw e;
   1165                             }
   1166                         }
   1167                     }
   1168                     if (supports_post_message(w) && w !== self) {
   1169                         w.postMessage({
   1170                             type: "result",
   1171                             test: test.structured_clone()
   1172                         }, "*");
   1173                     }
   1174                 });
   1175         this.processing_callbacks = false;
   1176         if (this_obj.all_done()) {
   1177             this_obj.complete();
   1178         }
   1179     };
   1180 
   1181     Tests.prototype.complete = function() {
   1182         if (this.phase === this.phases.COMPLETE) {
   1183             return;
   1184         }
   1185         this.phase = this.phases.COMPLETE;
   1186         var this_obj = this;
   1187         this.tests.forEach(
   1188             function(x)
   1189             {
   1190                 if (x.status === x.NOTRUN) {
   1191                     this_obj.notify_result(x);
   1192                     x.cleanup();
   1193                 }
   1194             }
   1195         );
   1196         this.notify_complete();
   1197     };
   1198 
   1199     Tests.prototype.notify_complete = function()
   1200     {
   1201         clearTimeout(this.timeout_id);
   1202         var this_obj = this;
   1203         var tests = map(this_obj.tests,
   1204                         function(test)
   1205                         {
   1206                             return test.structured_clone();
   1207                         });
   1208         if (this.status.status === null) {
   1209             this.status.status = this.status.OK;
   1210         }
   1211 
   1212         forEach (this.all_done_callbacks,
   1213                  function(callback)
   1214                  {
   1215                      callback(this_obj.tests, this_obj.status);
   1216                  });
   1217 
   1218         forEach_windows(
   1219                 function(w, is_same_origin)
   1220                 {
   1221                     if (is_same_origin && w.completion_callback) {
   1222                         try {
   1223                             w.completion_callback(this_obj.tests, this_obj.status);
   1224                         } catch (e) {
   1225                             if (debug) {
   1226                                 throw e;
   1227                             }
   1228                         }
   1229                     }
   1230                     if (supports_post_message(w) && w !== self) {
   1231                         w.postMessage({
   1232                             type: "complete",
   1233                             tests: tests,
   1234                             status: this_obj.status.structured_clone()
   1235                         }, "*");
   1236                     }
   1237                 });
   1238     };
   1239 
   1240     var tests = new Tests();
   1241 
   1242     addEventListener("error", function(e) {
   1243         if (tests.file_is_test) {
   1244             var test = tests.tests[0];
   1245             if (test.phase >= test.phases.HAS_RESULT) {
   1246                 return;
   1247             }
   1248             var message = e.message;
   1249             test.set_status(test.FAIL, message);
   1250             test.phase = test.phases.HAS_RESULT;
   1251             test.done();
   1252             done();
   1253         } else if (!tests.allow_uncaught_exception) {
   1254             tests.status.status = tests.status.ERROR;
   1255             tests.status.message = e.message;
   1256         }
   1257     });
   1258 
   1259     function timeout() {
   1260         if (tests.timeout_length === null) {
   1261             tests.timeout();
   1262         }
   1263     }
   1264     expose(timeout, 'timeout');
   1265 
   1266     function add_start_callback(callback) {
   1267         tests.start_callbacks.push(callback);
   1268     }
   1269 
   1270     function add_result_callback(callback)
   1271     {
   1272         tests.test_done_callbacks.push(callback);
   1273     }
   1274 
   1275     function add_completion_callback(callback)
   1276     {
   1277        tests.all_done_callbacks.push(callback);
   1278     }
   1279 
   1280     expose(add_start_callback, 'add_start_callback');
   1281     expose(add_result_callback, 'add_result_callback');
   1282     expose(add_completion_callback, 'add_completion_callback');
   1283 
   1284     /*
   1285      * Output listener
   1286     */
   1287 
   1288     function Output() {
   1289         this.output_document = document;
   1290         this.output_node = null;
   1291         this.done_count = 0;
   1292         this.enabled = settings.output;
   1293         this.phase = this.INITIAL;
   1294     }
   1295 
   1296     Output.prototype.INITIAL = 0;
   1297     Output.prototype.STARTED = 1;
   1298     Output.prototype.HAVE_RESULTS = 2;
   1299     Output.prototype.COMPLETE = 3;
   1300 
   1301     Output.prototype.setup = function(properties) {
   1302         if (this.phase > this.INITIAL) {
   1303             return;
   1304         }
   1305 
   1306         //If output is disabled in testharnessreport.js the test shouldn't be
   1307         //able to override that
   1308         this.enabled = this.enabled && (properties.hasOwnProperty("output") ?
   1309                                         properties.output : settings.output);
   1310     };
   1311 
   1312     Output.prototype.init = function(properties) {
   1313         if (this.phase >= this.STARTED) {
   1314             return;
   1315         }
   1316         if (properties.output_document) {
   1317             this.output_document = properties.output_document;
   1318         } else {
   1319             this.output_document = document;
   1320         }
   1321         this.phase = this.STARTED;
   1322     };
   1323 
   1324     Output.prototype.resolve_log = function() {
   1325         var output_document;
   1326         if (typeof this.output_document === "function") {
   1327             output_document = this.output_document.apply(undefined);
   1328         } else {
   1329             output_document = this.output_document;
   1330         }
   1331         if (!output_document) {
   1332             return;
   1333         }
   1334         var node = output_document.getElementById("log");
   1335         if (!node) {
   1336             if (!document.body || document.readyState == "loading") {
   1337                 return;
   1338             }
   1339             node = output_document.createElement("div");
   1340             node.id = "log";
   1341             output_document.body.appendChild(node);
   1342         }
   1343         this.output_document = output_document;
   1344         this.output_node = node;
   1345     };
   1346 
   1347     Output.prototype.show_status = function() {
   1348         if (this.phase < this.STARTED) {
   1349             this.init();
   1350         }
   1351         if (!this.enabled) {
   1352             return;
   1353         }
   1354         if (this.phase < this.HAVE_RESULTS) {
   1355             this.resolve_log();
   1356             this.phase = this.HAVE_RESULTS;
   1357         }
   1358         this.done_count++;
   1359         if (this.output_node) {
   1360             if (this.done_count < 100 ||
   1361                 (this.done_count < 1000 && this.done_count % 100 === 0) ||
   1362                 this.done_count % 1000 === 0) {
   1363                 this.output_node.textContent = "Running, " +
   1364                     this.done_count + " complete, " +
   1365                     tests.num_pending + " remain";
   1366             }
   1367         }
   1368     };
   1369 
   1370     Output.prototype.show_results = function (tests, harness_status) {
   1371         if (this.phase >= this.COMPLETE) {
   1372             return;
   1373         }
   1374         if (!this.enabled) {
   1375             return;
   1376         }
   1377         if (!this.output_node) {
   1378             this.resolve_log();
   1379         }
   1380         this.phase = this.COMPLETE;
   1381 
   1382         var log = this.output_node;
   1383         if (!log) {
   1384             return;
   1385         }
   1386         var output_document = this.output_document;
   1387 
   1388         while (log.lastChild) {
   1389             log.removeChild(log.lastChild);
   1390         }
   1391 
   1392         if (script_prefix != null) {
   1393             var stylesheet = output_document.createElementNS(xhtml_ns, "link");
   1394             stylesheet.setAttribute("rel", "stylesheet");
   1395             stylesheet.setAttribute("href", script_prefix + "testharness.css");
   1396             var heads = output_document.getElementsByTagName("head");
   1397             if (heads.length) {
   1398                 heads[0].appendChild(stylesheet);
   1399             }
   1400         }
   1401 
   1402         var status_text_harness = {};
   1403         status_text_harness[harness_status.OK] = "OK";
   1404         status_text_harness[harness_status.ERROR] = "Error";
   1405         status_text_harness[harness_status.TIMEOUT] = "Timeout";
   1406 
   1407         var status_text = {};
   1408         status_text[Test.prototype.PASS] = "Pass";
   1409         status_text[Test.prototype.FAIL] = "Fail";
   1410         status_text[Test.prototype.TIMEOUT] = "Timeout";
   1411         status_text[Test.prototype.NOTRUN] = "Not Run";
   1412 
   1413         var status_number = {};
   1414         forEach(tests,
   1415                 function(test) {
   1416                     var status = status_text[test.status];
   1417                     if (status_number.hasOwnProperty(status)) {
   1418                         status_number[status] += 1;
   1419                     } else {
   1420                         status_number[status] = 1;
   1421                     }
   1422                 });
   1423 
   1424         function status_class(status)
   1425         {
   1426             return status.replace(/\s/g, '').toLowerCase();
   1427         }
   1428 
   1429         var summary_template = ["section", {"id":"summary"},
   1430                                 ["h2", {}, "Summary"],
   1431                                 function()
   1432                                 {
   1433 
   1434                                     var status = status_text_harness[harness_status.status];
   1435                                     var rv = [["section", {},
   1436                                                ["p", {},
   1437                                                 "Harness status: ",
   1438                                                 ["span", {"class":status_class(status)},
   1439                                                  status
   1440                                                 ],
   1441                                                ]
   1442                                               ]];
   1443 
   1444                                     if (harness_status.status === harness_status.ERROR) {
   1445                                         rv[0].push(["pre", {}, harness_status.message]);
   1446                                     }
   1447                                     return rv;
   1448                                 },
   1449                                 ["p", {}, "Found ${num_tests} tests"],
   1450                                 function() {
   1451                                     var rv = [["div", {}]];
   1452                                     var i = 0;
   1453                                     while (status_text.hasOwnProperty(i)) {
   1454                                         if (status_number.hasOwnProperty(status_text[i])) {
   1455                                             var status = status_text[i];
   1456                                             rv[0].push(["div", {"class":status_class(status)},
   1457                                                         ["label", {},
   1458                                                          ["input", {type:"checkbox", checked:"checked"}],
   1459                                                          status_number[status] + " " + status]]);
   1460                                         }
   1461                                         i++;
   1462                                     }
   1463                                     return rv;
   1464                                 },
   1465                                ];
   1466 
   1467         log.appendChild(render(summary_template, {num_tests:tests.length}, output_document));
   1468 
   1469         forEach(output_document.querySelectorAll("section#summary label"),
   1470                 function(element)
   1471                 {
   1472                     on_event(element, "click",
   1473                              function(e)
   1474                              {
   1475                                  if (output_document.getElementById("results") === null) {
   1476                                      e.preventDefault();
   1477                                      return;
   1478                                  }
   1479                                  var result_class = element.parentNode.getAttribute("class");
   1480                                  var style_element = output_document.querySelector("style#hide-" + result_class);
   1481                                  var input_element = element.querySelector("input");
   1482                                  if (!style_element && !input_element.checked) {
   1483                                      style_element = output_document.createElementNS(xhtml_ns, "style");
   1484                                      style_element.id = "hide-" + result_class;
   1485                                      style_element.textContent = "table#results > tbody > tr."+result_class+"{display:none}";
   1486                                      output_document.body.appendChild(style_element);
   1487                                  } else if (style_element && input_element.checked) {
   1488                                      style_element.parentNode.removeChild(style_element);
   1489                                  }
   1490                              });
   1491                 });
   1492 
   1493         // This use of innerHTML plus manual escaping is not recommended in
   1494         // general, but is necessary here for performance.  Using textContent
   1495         // on each individual <td> adds tens of seconds of execution time for
   1496         // large test suites (tens of thousands of tests).
   1497         function escape_html(s)
   1498         {
   1499             return s.replace(/\&/g, "&amp;")
   1500                 .replace(/</g, "&lt;")
   1501                 .replace(/"/g, "&quot;")
   1502                 .replace(/'/g, "&#39;");
   1503         }
   1504 
   1505         function has_assertions()
   1506         {
   1507             for (var i = 0; i < tests.length; i++) {
   1508                 if (tests[i].properties.hasOwnProperty("assert")) {
   1509                     return true;
   1510                 }
   1511             }
   1512             return false;
   1513         }
   1514 
   1515         function get_assertion(test)
   1516         {
   1517             if (test.properties.hasOwnProperty("assert")) {
   1518                 if (Array.isArray(test.properties.assert)) {
   1519                     return test.properties.assert.join(' ');
   1520                 }
   1521                 return test.properties.assert;
   1522             }
   1523             return '';
   1524         }
   1525 
   1526         log.appendChild(document.createElementNS(xhtml_ns, "section"));
   1527         var assertions = has_assertions();
   1528         var html = "<h2>Details</h2><table id='results' " + (assertions ? "class='assertions'" : "" ) + ">" +
   1529             "<thead><tr><th>Result</th><th>Test Name</th>" +
   1530             (assertions ? "<th>Assertion</th>" : "") +
   1531             "<th>Message</th></tr></thead>" +
   1532             "<tbody>";
   1533         for (var i = 0; i < tests.length; i++) {
   1534             html += '<tr class="' +
   1535                 escape_html(status_class(status_text[tests[i].status])) +
   1536                 '"><td>' +
   1537                 escape_html(status_text[tests[i].status]) +
   1538                 "</td><td>" +
   1539                 escape_html(tests[i].name) +
   1540                 "</td><td>" +
   1541                 (assertions ? escape_html(get_assertion(tests[i])) + "</td><td>" : "") +
   1542                 escape_html(tests[i].message ? tests[i].message : " ") +
   1543                 "</td></tr>";
   1544         }
   1545         html += "</tbody></table>";
   1546         try {
   1547             log.lastChild.innerHTML = html;
   1548         } catch (e) {
   1549             log.appendChild(document.createElementNS(xhtml_ns, "p"))
   1550                .textContent = "Setting innerHTML for the log threw an exception.";
   1551             log.appendChild(document.createElementNS(xhtml_ns, "pre"))
   1552                .textContent = html;
   1553         }
   1554     };
   1555 
   1556     var output = new Output();
   1557     add_start_callback(function (properties) {output.init(properties);});
   1558     add_result_callback(function () {output.show_status();});
   1559     add_completion_callback(function (tests, harness_status) {output.show_results(tests, harness_status);});
   1560 
   1561     /*
   1562      * Template code
   1563      *
   1564      * A template is just a javascript structure. An element is represented as:
   1565      *
   1566      * [tag_name, {attr_name:attr_value}, child1, child2]
   1567      *
   1568      * the children can either be strings (which act like text nodes), other templates or
   1569      * functions (see below)
   1570      *
   1571      * A text node is represented as
   1572      *
   1573      * ["{text}", value]
   1574      *
   1575      * String values have a simple substitution syntax; ${foo} represents a variable foo.
   1576      *
   1577      * It is possible to embed logic in templates by using a function in a place where a
   1578      * node would usually go. The function must either return part of a template or null.
   1579      *
   1580      * In cases where a set of nodes are required as output rather than a single node
   1581      * with children it is possible to just use a list
   1582      * [node1, node2, node3]
   1583      *
   1584      * Usage:
   1585      *
   1586      * render(template, substitutions) - take a template and an object mapping
   1587      * variable names to parameters and return either a DOM node or a list of DOM nodes
   1588      *
   1589      * substitute(template, substitutions) - take a template and variable mapping object,
   1590      * make the variable substitutions and return the substituted template
   1591      *
   1592      */
   1593 
   1594     function is_single_node(template)
   1595     {
   1596         return typeof template[0] === "string";
   1597     }
   1598 
   1599     function substitute(template, substitutions)
   1600     {
   1601         if (typeof template === "function") {
   1602             var replacement = template(substitutions);
   1603             if (!replacement) {
   1604                 return null;
   1605             }
   1606 
   1607             return substitute(replacement, substitutions);
   1608         }
   1609 
   1610         if (is_single_node(template)) {
   1611             return substitute_single(template, substitutions);
   1612         }
   1613 
   1614         return filter(map(template, function(x) {
   1615                               return substitute(x, substitutions);
   1616                           }), function(x) {return x !== null;});
   1617     }
   1618 
   1619     function substitute_single(template, substitutions)
   1620     {
   1621         var substitution_re = /\$\{([^ }]*)\}/g;
   1622 
   1623         function do_substitution(input) {
   1624             var components = input.split(substitution_re);
   1625             var rv = [];
   1626             for (var i = 0; i < components.length; i += 2) {
   1627                 rv.push(components[i]);
   1628                 if (components[i + 1]) {
   1629                     rv.push(String(substitutions[components[i + 1]]));
   1630                 }
   1631             }
   1632             return rv;
   1633         }
   1634 
   1635         function substitute_attrs(attrs, rv)
   1636         {
   1637             rv[1] = {};
   1638             for (var name in template[1]) {
   1639                 if (attrs.hasOwnProperty(name)) {
   1640                     var new_name = do_substitution(name).join("");
   1641                     var new_value = do_substitution(attrs[name]).join("");
   1642                     rv[1][new_name] = new_value;
   1643                 }
   1644             }
   1645         }
   1646 
   1647         function substitute_children(children, rv)
   1648         {
   1649             for (var i = 0; i < children.length; i++) {
   1650                 if (children[i] instanceof Object) {
   1651                     var replacement = substitute(children[i], substitutions);
   1652                     if (replacement !== null) {
   1653                         if (is_single_node(replacement)) {
   1654                             rv.push(replacement);
   1655                         } else {
   1656                             extend(rv, replacement);
   1657                         }
   1658                     }
   1659                 } else {
   1660                     extend(rv, do_substitution(String(children[i])));
   1661                 }
   1662             }
   1663             return rv;
   1664         }
   1665 
   1666         var rv = [];
   1667         rv.push(do_substitution(String(template[0])).join(""));
   1668 
   1669         if (template[0] === "{text}") {
   1670             substitute_children(template.slice(1), rv);
   1671         } else {
   1672             substitute_attrs(template[1], rv);
   1673             substitute_children(template.slice(2), rv);
   1674         }
   1675 
   1676         return rv;
   1677     }
   1678 
   1679     function make_dom_single(template, doc)
   1680     {
   1681         var output_document = doc || document;
   1682         var element;
   1683         if (template[0] === "{text}") {
   1684             element = output_document.createTextNode("");
   1685             for (var i = 1; i < template.length; i++) {
   1686                 element.data += template[i];
   1687             }
   1688         } else {
   1689             element = output_document.createElementNS(xhtml_ns, template[0]);
   1690             for (var name in template[1]) {
   1691                 if (template[1].hasOwnProperty(name)) {
   1692                     element.setAttribute(name, template[1][name]);
   1693                 }
   1694             }
   1695             for (var i = 2; i < template.length; i++) {
   1696                 if (template[i] instanceof Object) {
   1697                     var sub_element = make_dom(template[i]);
   1698                     element.appendChild(sub_element);
   1699                 } else {
   1700                     var text_node = output_document.createTextNode(template[i]);
   1701                     element.appendChild(text_node);
   1702                 }
   1703             }
   1704         }
   1705 
   1706         return element;
   1707     }
   1708 
   1709 
   1710 
   1711     function make_dom(template, substitutions, output_document)
   1712     {
   1713         if (is_single_node(template)) {
   1714             return make_dom_single(template, output_document);
   1715         }
   1716 
   1717         return map(template, function(x) {
   1718                        return make_dom_single(x, output_document);
   1719                    });
   1720     }
   1721 
   1722     function render(template, substitutions, output_document)
   1723     {
   1724         return make_dom(substitute(template, substitutions), output_document);
   1725     }
   1726 
   1727     /*
   1728      * Utility funcions
   1729      */
   1730     function assert(expected_true, function_name, description, error, substitutions)
   1731     {
   1732         if (tests.tests.length === 0) {
   1733             tests.set_file_is_test();
   1734         }
   1735         if (expected_true !== true) {
   1736             var msg = make_message(function_name, description,
   1737                                    error, substitutions);
   1738             throw new AssertionError(msg);
   1739         }
   1740     }
   1741 
   1742     function AssertionError(message)
   1743     {
   1744         this.message = message;
   1745     }
   1746 
   1747     AssertionError.prototype.toString = function() {
   1748         return this.message;
   1749     };
   1750 
   1751     function make_message(function_name, description, error, substitutions)
   1752     {
   1753         for (var p in substitutions) {
   1754             if (substitutions.hasOwnProperty(p)) {
   1755                 substitutions[p] = format_value(substitutions[p]);
   1756             }
   1757         }
   1758         var node_form = substitute(["{text}", "${function_name}: ${description}" + error],
   1759                                    merge({function_name:function_name,
   1760                                           description:(description?description + " ":"")},
   1761                                           substitutions));
   1762         return node_form.slice(1).join("");
   1763     }
   1764 
   1765     function filter(array, callable, thisObj) {
   1766         var rv = [];
   1767         for (var i = 0; i < array.length; i++) {
   1768             if (array.hasOwnProperty(i)) {
   1769                 var pass = callable.call(thisObj, array[i], i, array);
   1770                 if (pass) {
   1771                     rv.push(array[i]);
   1772                 }
   1773             }
   1774         }
   1775         return rv;
   1776     }
   1777 
   1778     function map(array, callable, thisObj)
   1779     {
   1780         var rv = [];
   1781         rv.length = array.length;
   1782         for (var i = 0; i < array.length; i++) {
   1783             if (array.hasOwnProperty(i)) {
   1784                 rv[i] = callable.call(thisObj, array[i], i, array);
   1785             }
   1786         }
   1787         return rv;
   1788     }
   1789 
   1790     function extend(array, items)
   1791     {
   1792         Array.prototype.push.apply(array, items);
   1793     }
   1794 
   1795     function forEach (array, callback, thisObj)
   1796     {
   1797         for (var i = 0; i < array.length; i++) {
   1798             if (array.hasOwnProperty(i)) {
   1799                 callback.call(thisObj, array[i], i, array);
   1800             }
   1801         }
   1802     }
   1803 
   1804     function merge(a,b)
   1805     {
   1806         var rv = {};
   1807         var p;
   1808         for (p in a) {
   1809             rv[p] = a[p];
   1810         }
   1811         for (p in b) {
   1812             rv[p] = b[p];
   1813         }
   1814         return rv;
   1815     }
   1816 
   1817     function expose(object, name)
   1818     {
   1819         var components = name.split(".");
   1820         var target = window;
   1821         for (var i = 0; i < components.length - 1; i++) {
   1822             if (!(components[i] in target)) {
   1823                 target[components[i]] = {};
   1824             }
   1825             target = target[components[i]];
   1826         }
   1827         target[components[components.length - 1]] = object;
   1828     }
   1829 
   1830     function forEach_windows(callback) {
   1831         // Iterate of the the windows [self ... top, opener]. The callback is passed
   1832         // two objects, the first one is the windows object itself, the second one
   1833         // is a boolean indicating whether or not its on the same origin as the
   1834         // current window.
   1835         var cache = forEach_windows.result_cache;
   1836         if (!cache) {
   1837             cache = [[self, true]];
   1838             var w = self;
   1839             var i = 0;
   1840             var so;
   1841             var origins = location.ancestorOrigins;
   1842             while (w != w.parent) {
   1843                 w = w.parent;
   1844                 // In WebKit, calls to parent windows' properties that aren't on the same
   1845                 // origin cause an error message to be displayed in the error console but
   1846                 // don't throw an exception. This is a deviation from the current HTML5
   1847                 // spec. See: https://bugs.webkit.org/show_bug.cgi?id=43504
   1848                 // The problem with WebKit's behavior is that it pollutes the error console
   1849                 // with error messages that can't be caught.
   1850                 //
   1851                 // This issue can be mitigated by relying on the (for now) proprietary
   1852                 // `location.ancestorOrigins` property which returns an ordered list of
   1853                 // the origins of enclosing windows. See:
   1854                 // http://trac.webkit.org/changeset/113945.
   1855                 if (origins) {
   1856                     so = (location.origin == origins[i]);
   1857                 } else {
   1858                     so = is_same_origin(w);
   1859                 }
   1860                 cache.push([w, so]);
   1861                 i++;
   1862             }
   1863             w = window.opener;
   1864             if (w) {
   1865                 // window.opener isn't included in the `location.ancestorOrigins` prop.
   1866                 // We'll just have to deal with a simple check and an error msg on WebKit
   1867                 // browsers in this case.
   1868                 cache.push([w, is_same_origin(w)]);
   1869             }
   1870             forEach_windows.result_cache = cache;
   1871         }
   1872 
   1873         forEach(cache,
   1874                 function(a)
   1875                 {
   1876                     callback.apply(null, a);
   1877                 });
   1878     }
   1879 
   1880     function is_same_origin(w) {
   1881         try {
   1882             'random_prop' in w;
   1883             return true;
   1884         } catch (e) {
   1885             return false;
   1886         }
   1887     }
   1888 
   1889     function supports_post_message(w)
   1890     {
   1891         var supports;
   1892         var type;
   1893         // Given IE  implements postMessage across nested iframes but not across
   1894         // windows or tabs, you can't infer cross-origin communication from the presence
   1895         // of postMessage on the current window object only.
   1896         //
   1897         // Touching the postMessage prop on a window can throw if the window is
   1898         // not from the same origin AND post message is not supported in that
   1899         // browser. So just doing an existence test here won't do, you also need
   1900         // to wrap it in a try..cacth block.
   1901         try {
   1902             type = typeof w.postMessage;
   1903             if (type === "function") {
   1904                 supports = true;
   1905             }
   1906 
   1907             // IE8 supports postMessage, but implements it as a host object which
   1908             // returns "object" as its `typeof`.
   1909             else if (type === "object") {
   1910                 supports = true;
   1911             }
   1912 
   1913             // This is the case where postMessage isn't supported AND accessing a
   1914             // window property across origins does NOT throw (e.g. old Safari browser).
   1915             else {
   1916                 supports = false;
   1917             }
   1918         } catch (e) {
   1919             // This is the case where postMessage isn't supported AND accessing a
   1920             // window property across origins throws (e.g. old Firefox browser).
   1921             supports = false;
   1922         }
   1923         return supports;
   1924     }
   1925 })();
   1926 // vim: set expandtab shiftwidth=4 tabstop=4:
   1927