• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 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