tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

runner.js (30615B)


      1 /*jshint nonew: false */
      2 (function() {
      3 "use strict";
      4 var runner;
      5 var testharness_properties = {output:false,
      6                              timeout_multiplier:1};
      7 
      8 function Manifest(path) {
      9    this.data = null;
     10    this.path = path;
     11    this.num_tests = null;
     12 }
     13 
     14 Manifest.prototype = {
     15    load: function(loaded_callback) {
     16        this.generate(loaded_callback);
     17    },
     18 
     19    generate: function(loaded_callback) {
     20        var xhr = new XMLHttpRequest();
     21        xhr.onreadystatechange = function() {
     22            if (xhr.readyState !== 4) {
     23                return;
     24            }
     25            if (!(xhr.status === 200 || xhr.status === 0)) {
     26                throw new Error("Manifest generation failed");
     27            }
     28            this.data = JSON.parse(xhr.responseText);
     29            loaded_callback();
     30        }.bind(this);
     31        xhr.open("POST", "update_manifest.py");
     32        xhr.send(null);
     33    },
     34 
     35    by_type:function(type) {
     36        var ret = [] ;
     37        if (this.data.items.hasOwnProperty(type)) {
     38            for (var propertyName in this.data.items[type]) {
     39                var arr = this.data.items[type][propertyName][0];
     40                var item = arr[arr.length - 1];
     41                item.path = propertyName;
     42                if ('string' === typeof arr[0]) {
     43                    item.url = arr[0];
     44                }
     45                if (Array.isArray(arr[1])) {
     46                    item.references = arr[1];
     47                }
     48                ret.push(item);
     49            }
     50        }
     51        return ret ;
     52    }
     53 };
     54 
     55 function ManifestIterator(manifest, path, test_types, use_regex) {
     56    this.manifest = manifest;
     57    this.paths = null;
     58    this.regex_pattern = null;
     59    this.test_types = test_types;
     60    this.test_types_index = -1;
     61    this.test_list = null;
     62    this.test_index = null;
     63 
     64    if (use_regex) {
     65        this.regex_pattern = path;
     66    } else {
     67        // Split paths by either a comma or whitespace, and ignore empty sub-strings.
     68        this.paths = path.split(/[,\s]+/).filter(function(s) { return s.length > 0; });
     69    }
     70 }
     71 
     72 ManifestIterator.prototype = {
     73    next: function() {
     74        var manifest_item = null;
     75 
     76        if (this.test_types.length === 0) {
     77            return null;
     78        }
     79 
     80        while (!manifest_item) {
     81            while (this.test_list === null || this.test_index >= this.test_list.length) {
     82                this.test_types_index++;
     83                if (this.test_types_index >= this.test_types.length) {
     84                    return null;
     85                }
     86                this.test_index = 0;
     87                this.test_list = this.manifest.by_type(this.test_types[this.test_types_index]);
     88            }
     89 
     90            manifest_item = this.test_list[this.test_index++];
     91            while (manifest_item && !this.matches(manifest_item)) {
     92                manifest_item = this.test_list[this.test_index++];
     93            }
     94            if (manifest_item) {
     95                return this.to_test(manifest_item);
     96            }
     97        }
     98    },
     99 
    100    // Calculate the location of a match within a provided URL.
    101    //
    102    // @param {string} url - Valid URL
    103    //
    104    // @returns {null|object} - null if the URL does not satisfy the iterator's
    105    //                          filtering criteria. Otherwise, an object with
    106    //                          the following properties:
    107    //
    108    //                          - index - the zero-indexed offset of the start
    109    //                                    of the match
    110    //                          - width - the total number of matching
    111    //                                    characters
    112    match_location: function(url) {
    113        var match;
    114 
    115        if (this.regex_pattern) {
    116           match = url.match(this.regex_pattern);
    117 
    118           if (!match) {
    119              return null;
    120           }
    121 
    122           return { index: match.index, width: match[0].length };
    123        }
    124 
    125        this.paths.some(function(path) {
    126            if (url.indexOf(path) === 0) {
    127                match = path;
    128                return true;
    129            }
    130            return false;
    131        });
    132 
    133        if (!match) {
    134            return null;
    135        }
    136 
    137        return { index: 0, width: match.length };
    138    },
    139 
    140    matches: function(manifest_item) {
    141        var url_base = this.manifest.data.url_base;
    142        if (url_base.charAt(url_base.length - 1) !== "/") {
    143            url_base = url_base + "/";
    144        }
    145        var url = url_base + manifest_item.url;
    146        return this.match_location(url) !== null;
    147    },
    148 
    149    to_test: function(manifest_item) {
    150        var url_base = this.manifest.data.url_base;
    151        if (url_base.charAt(url_base.length - 1) !== "/") {
    152            url_base = url_base + "/";
    153        }
    154        var test = {
    155            type: this.test_types[this.test_types_index],
    156            url: url_base + manifest_item.url
    157        };
    158        if (manifest_item.hasOwnProperty("references")) {
    159            test.ref_length = manifest_item.references.length;
    160            test.ref_type = manifest_item.references[0][1];
    161            test.ref_url = manifest_item.references[0][0];
    162        }
    163        return test;
    164    },
    165 
    166    count: function() {
    167        return this.test_types.reduce(function(prev, current) {
    168            var matches = this.manifest.by_type(current).filter(function(x) {
    169                return this.matches(x);
    170            }.bind(this));
    171            return prev + matches.length;
    172        }.bind(this), 0);
    173    }
    174 };
    175 
    176 function VisualOutput(elem, runner) {
    177    this.elem = elem;
    178    this.runner = runner;
    179    this.results_table = null;
    180    this.section_wrapper = null;
    181    this.results_table = this.elem.querySelector(".results > table");
    182    this.section = null;
    183    this.progress = this.elem.querySelector(".summary .progress");
    184    this.meter = this.progress.querySelector(".progress-bar");
    185    this.result_count = null;
    186    this.json_results_area = this.elem.querySelector("textarea");
    187    this.instructions = document.querySelector(".instructions");
    188 
    189    this.elem.style.display = "none";
    190    this.runner.manifest_wait_callbacks.push(this.on_manifest_wait.bind(this));
    191    this.runner.start_callbacks.push(this.on_start.bind(this));
    192    this.runner.result_callbacks.push(this.on_result.bind(this));
    193    this.runner.done_callbacks.push(this.on_done.bind(this));
    194 
    195    this.display_filter_state = {};
    196 
    197    var visual_output = this;
    198    var display_filter_inputs = this.elem.querySelectorAll(".result-display-filter");
    199    for (var i = 0; i < display_filter_inputs.length; ++i) {
    200        var display_filter_input = display_filter_inputs[i];
    201        this.display_filter_state[display_filter_input.value] = display_filter_input.checked;
    202        display_filter_input.addEventListener("change", function(e) {
    203            visual_output.apply_display_filter(e.target.value, e.target.checked);
    204        })
    205    }
    206 }
    207 
    208 VisualOutput.prototype = {
    209    clear: function() {
    210        this.result_count = {"PASS":0,
    211                             "FAIL":0,
    212                             "ERROR":0,
    213                             "TIMEOUT":0,
    214                             "NOTRUN":0};
    215        for (var p in this.result_count) {
    216            if (this.result_count.hasOwnProperty(p)) {
    217                this.elem.querySelector("td." + p).textContent = 0;
    218            }
    219        }
    220        if (this.json_results_area) {
    221            this.json_results_area.parentNode.removeChild(this.json_results_area);
    222        }
    223        this.meter.style.width = '0px';
    224        this.meter.textContent = '0%';
    225        this.meter.classList.remove("stopped", "loading-manifest");
    226        this.elem.querySelector(".jsonResults").style.display = "none";
    227        this.results_table.removeChild(this.results_table.tBodies[0]);
    228        this.results_table.appendChild(document.createElement("tbody"));
    229    },
    230 
    231    on_manifest_wait: function() {
    232        this.clear();
    233        this.instructions.style.display = "none";
    234        this.elem.style.display = "block";
    235        this.steady_status("loading-manifest");
    236    },
    237 
    238    on_start: function() {
    239        this.clear();
    240        this.instructions.style.display = "none";
    241        this.elem.style.display = "block";
    242        this.meter.classList.add("progress-striped", "active");
    243    },
    244 
    245    on_result: function(test, status, message, subtests) {
    246        var row = document.createElement("tr");
    247 
    248        var subtest_pass_count = subtests.reduce(function(prev, current) {
    249            return (current.status === "PASS") ? prev + 1 : prev;
    250        }, 0);
    251 
    252        var subtest_notrun_count = subtests.reduce(function(prev, current) {
    253            return (current.status === "NOTRUN") ? prev +1 : prev;
    254        }, 0);
    255 
    256        var subtests_count = subtests.length;
    257 
    258        var test_status;
    259        if (subtest_pass_count === subtests_count &&
    260            (status == "OK" || status == "PASS")) {
    261            test_status = "PASS";
    262        } else if ((!subtests_count && status === "NOTRUN") ||
    263            (subtests_count && (subtest_notrun_count == subtests_count) ) ) {
    264            test_status = "NOTRUN";
    265        } else if (subtests_count > 0 && status === "OK") {
    266            test_status = "FAIL";
    267        } else {
    268            test_status = status;
    269        }
    270 
    271        subtests.forEach(function(subtest) {
    272            if (this.result_count.hasOwnProperty(subtest.status)) {
    273                this.result_count[subtest.status] += 1;
    274            }
    275        }.bind(this));
    276        if (this.result_count.hasOwnProperty(status)) {
    277            this.result_count[status] += 1;
    278        }
    279 
    280        var name_node = row.appendChild(document.createElement("td"));
    281        name_node.appendChild(this.test_name_node(test));
    282 
    283        var status_node = row.appendChild(document.createElement("td"));
    284        status_node.textContent = test_status;
    285        status_node.className = test_status;
    286 
    287        var message_node = row.appendChild(document.createElement("td"));
    288        message_node.textContent = message || "";
    289 
    290        var subtests_node = row.appendChild(document.createElement("td"));
    291        if (subtests_count) {
    292            subtests_node.textContent = subtest_pass_count + "/" + subtests_count;
    293        } else {
    294            if (status == "PASS") {
    295                subtests_node.textContent = "1/1";
    296            } else {
    297                subtests_node.textContent = "0/1";
    298            }
    299        }
    300 
    301        var status_arr = ["PASS", "FAIL", "ERROR", "TIMEOUT", "NOTRUN"];
    302        for (var i = 0; i < status_arr.length; i++) {
    303            this.elem.querySelector("td." + status_arr[i]).textContent = this.result_count[status_arr[i]];
    304        }
    305 
    306        this.apply_display_filter_to_result_row(row, this.display_filter_state[test_status]);
    307        this.results_table.tBodies[0].appendChild(row);
    308        this.update_meter(this.runner.progress(), this.runner.results.count(), this.runner.test_count());
    309    },
    310 
    311    steady_status: function(statusName) {
    312        var statusTexts = {
    313            done: "Done!",
    314            stopped: "Stopped",
    315            "loading-manifest": "Updating and loading test manifest; this may take several minutes."
    316        };
    317        var textContent = statusTexts[statusName];
    318 
    319        this.meter.setAttribute("aria-valuenow", this.meter.getAttribute("aria-valuemax"));
    320        this.meter.style.width = "100%";
    321        this.meter.textContent = textContent;
    322        this.meter.classList.remove("progress-striped", "active", "stopped", "loading-manifest");
    323        this.meter.classList.add(statusName);
    324        this.runner.display_current_test(null);
    325    },
    326 
    327    on_done: function() {
    328        this.steady_status(this.runner.stop_flag ? "stopped" : "done");
    329        //add the json serialization of the results
    330        var a = this.elem.querySelector(".jsonResults");
    331        var json = this.runner.results.to_json();
    332 
    333        if (document.getElementById("dumpit").checked) {
    334            this.json_results_area = Array.prototype.slice.call(this.elem.querySelectorAll("textarea"));
    335            for(var i = 0,t = this.json_results_area.length; i < t; i++){
    336                this.elem.removeChild(this.json_results_area[i]);
    337            }
    338            this.json_results_area = document.createElement("textarea");
    339            this.json_results_area.style.width = "100%";
    340            this.json_results_area.setAttribute("rows", "50");
    341            this.elem.appendChild(this.json_results_area);
    342            this.json_results_area.textContent = json;
    343        }
    344        var blob = new Blob([json], { type: "application/json" });
    345        a.href = window.URL.createObjectURL(blob);
    346        a.download = "runner-results.json";
    347        a.textContent = "Download JSON results";
    348        if (!a.getAttribute("download")) a.textContent += " (right-click and save as to download)";
    349        a.style.display = "inline";
    350    },
    351 
    352    test_name_node: function(test) {
    353        if (!test.hasOwnProperty("ref_url")) {
    354            return this.link(test.url);
    355        } else {
    356            var wrapper = document.createElement("span");
    357            wrapper.appendChild(this.link(test.url));
    358            wrapper.appendChild(document.createTextNode(" " + test.ref_type + " "));
    359            wrapper.appendChild(this.link(test.ref_url));
    360            return wrapper;
    361        }
    362    },
    363 
    364    link: function(href) {
    365        var link = document.createElement("a");
    366        link.href = this.runner.server + href;
    367        link.textContent = href;
    368        return link;
    369    },
    370 
    371    update_meter: function(progress, count, total) {
    372        this.meter.setAttribute("aria-valuenow", count);
    373        this.meter.setAttribute("aria-valuemax", total);
    374        this.meter.textContent = this.meter.style.width = (progress * 100).toFixed(1) + "%";
    375    },
    376 
    377    apply_display_filter: function(test_status, display_state) {
    378        this.display_filter_state[test_status] = display_state;
    379        var result_cells = this.elem.querySelectorAll(".results > table tr td." + test_status);
    380        for (var i = 0; i < result_cells.length; ++i) {
    381            this.apply_display_filter_to_result_row(result_cells[i].parentNode, display_state)
    382        }
    383    },
    384 
    385    apply_display_filter_to_result_row: function(result_row, display_state) {
    386        result_row.style.display = display_state ? "" : "none";
    387    }
    388 };
    389 
    390 function ManualUI(elem, runner) {
    391    this.elem = elem;
    392    this.runner = runner;
    393    this.pass_button = this.elem.querySelector("button.pass");
    394    this.fail_button = this.elem.querySelector("button.fail");
    395    this.skip_button = this.elem.querySelector("button.skip");
    396    this.ref_buttons = this.elem.querySelector(".reftestUI");
    397    this.ref_type = this.ref_buttons.querySelector(".refType");
    398    this.ref_warning = this.elem.querySelector(".reftestWarn");
    399    this.test_button = this.ref_buttons.querySelector("button.test");
    400    this.ref_button = this.ref_buttons.querySelector("button.ref");
    401 
    402    this.hide();
    403 
    404    this.runner.test_start_callbacks.push(this.on_test_start.bind(this));
    405    this.runner.test_pause_callbacks.push(this.hide.bind(this));
    406    this.runner.done_callbacks.push(this.on_done.bind(this));
    407 
    408    this.pass_button.onclick = function() {
    409        this.disable_buttons();
    410        this.runner.on_result("PASS", "", []);
    411    }.bind(this);
    412 
    413    this.skip_button.onclick = function() {
    414        this.disable_buttons();
    415        this.runner.on_result("NOTRUN", "", []);
    416    }.bind(this);
    417 
    418    this.fail_button.onclick = function() {
    419        this.disable_buttons();
    420        this.runner.on_result("FAIL", "", []);
    421    }.bind(this);
    422 }
    423 
    424 ManualUI.prototype = {
    425    show: function() {
    426        this.elem.style.display = "block";
    427        setTimeout(this.enable_buttons.bind(this), 200);
    428    },
    429 
    430    hide: function() {
    431        this.elem.style.display = "none";
    432    },
    433 
    434    show_ref: function() {
    435        this.ref_buttons.style.display = "block";
    436        this.test_button.onclick = function() {
    437            this.runner.load(this.runner.current_test.url);
    438        }.bind(this);
    439        this.ref_button.onclick = function() {
    440            this.runner.load(this.runner.current_test.ref_url);
    441        }.bind(this);
    442    },
    443 
    444    hide_ref: function() {
    445        this.ref_buttons.style.display = "none";
    446    },
    447 
    448    disable_buttons: function() {
    449        this.pass_button.disabled = true;
    450        this.fail_button.disabled = true;
    451    },
    452 
    453    enable_buttons: function() {
    454        this.pass_button.disabled = false;
    455        this.fail_button.disabled = false;
    456    },
    457 
    458    on_test_start: function(test) {
    459        if (test.type == "manual" || test.type == "reftest") {
    460            this.show();
    461        } else {
    462            this.hide();
    463        }
    464        if (test.type == "reftest") {
    465            this.show_ref();
    466            this.ref_type.textContent = test.ref_type === "==" ? "equal" : "unequal";
    467            if (test.ref_length > 1) {
    468                this.ref_warning.textContent = "WARNING: only presenting first of " + test.ref_length + " references";
    469                this.ref_warning.style.display = "inline";
    470            }  else {
    471                this.ref_warning.textContent = "";
    472                this.ref_warning.style.display = "none";
    473            }
    474        } else {
    475            this.hide_ref();
    476        }
    477    },
    478 
    479    on_done: function() {
    480        this.hide();
    481    }
    482 };
    483 
    484 function TestControl(elem, runner) {
    485    this.elem = elem;
    486    this.path_input = this.elem.querySelector(".path");
    487    this.path_input.addEventListener("change", function() {
    488        this.set_counts();
    489    }.bind(this), false);
    490    this.use_regex_input = this.elem.querySelector("#use_regex");
    491    this.use_regex_input.addEventListener("change", function() {
    492        this.set_counts();
    493    }.bind(this), false);
    494    this.pause_button = this.elem.querySelector("button.togglePause");
    495    this.start_button = this.elem.querySelector("button.toggleStart");
    496    this.type_checkboxes = Array.prototype.slice.call(
    497        this.elem.querySelectorAll("input[type=checkbox].test-type"));
    498    this.type_checkboxes.forEach(function(elem) {
    499        elem.addEventListener("change", function() {
    500            this.set_counts();
    501        }.bind(this),
    502        false);
    503        elem.addEventListener("click", function() {
    504            this.start_button.disabled = this.get_test_types().length < 1;
    505        }.bind(this),
    506        false);
    507    }.bind(this));
    508 
    509    this.timeout_input = this.elem.querySelector(".timeout_multiplier");
    510    this.render_checkbox = this.elem.querySelector(".render");
    511    this.testcount_area = this.elem.querySelector("#testcount");
    512    this.runner = runner;
    513    this.runner.done_callbacks.push(this.on_done.bind(this));
    514    this.set_start();
    515    this.set_counts();
    516 }
    517 
    518 TestControl.prototype = {
    519    set_start: function() {
    520        this.start_button.disabled = this.get_test_types().length < 1;
    521        this.pause_button.disabled = true;
    522        this.start_button.textContent = "Start";
    523        this.path_input.disabled = false;
    524        this.type_checkboxes.forEach(function(elem) {
    525            elem.disabled = false;
    526        });
    527        this.start_button.onclick = function() {
    528            var path = this.get_path();
    529            var test_types = this.get_test_types();
    530            var settings = this.get_testharness_settings();
    531            var use_regex = this.get_use_regex();
    532            this.runner.start(path, test_types, settings, use_regex);
    533            this.set_stop();
    534            this.set_pause();
    535        }.bind(this);
    536    },
    537 
    538    set_stop: function() {
    539        clearTimeout(this.runner.timeout);
    540        this.pause_button.disabled = false;
    541        this.start_button.textContent = "Stop";
    542        this.path_input.disabled = true;
    543        this.type_checkboxes.forEach(function(elem) {
    544            elem.disabled = true;
    545        });
    546        this.start_button.onclick = function() {
    547            this.runner.stop_flag = true;
    548            this.runner.done();
    549        }.bind(this);
    550    },
    551 
    552    set_pause: function() {
    553        this.pause_button.textContent = "Pause";
    554        this.pause_button.onclick = function() {
    555            this.runner.pause();
    556            this.set_resume();
    557        }.bind(this);
    558    },
    559 
    560    set_resume: function() {
    561        this.pause_button.textContent = "Resume";
    562        this.pause_button.onclick = function() {
    563            this.runner.unpause();
    564            this.set_pause();
    565        }.bind(this);
    566 
    567    },
    568 
    569    set_counts: function() {
    570        if (this.runner.manifest_loading) {
    571            setTimeout(function() {
    572                this.set_counts();
    573            }.bind(this), 1000);
    574            return;
    575        }
    576        var path = this.get_path();
    577        var test_types = this.get_test_types();
    578        var use_regex = this.get_use_regex();
    579        var iterator = new ManifestIterator(this.runner.manifest, path, test_types, use_regex);
    580        var count = iterator.count();
    581        this.testcount_area.textContent = count;
    582    },
    583 
    584    get_path: function() {
    585        return this.path_input.value;
    586    },
    587 
    588    get_test_types: function() {
    589        return this.type_checkboxes.filter(function(elem) {
    590            return elem.checked;
    591        }).map(function(elem) {
    592            return elem.value;
    593        });
    594    },
    595 
    596    get_testharness_settings: function() {
    597        return {timeout_multiplier: parseFloat(this.timeout_input.value),
    598                output: this.render_checkbox.checked};
    599    },
    600 
    601    get_use_regex: function() {
    602        return this.use_regex_input.checked;
    603    },
    604 
    605    on_done: function() {
    606        this.set_pause();
    607        this.set_start();
    608    }
    609 };
    610 
    611 function Results(runner) {
    612    this.test_results = null;
    613    this.runner = runner;
    614 
    615    this.runner.start_callbacks.push(this.on_start.bind(this));
    616 }
    617 
    618 Results.prototype = {
    619    on_start: function() {
    620        this.test_results = [];
    621    },
    622 
    623    set: function(test, status, message, subtests) {
    624        this.test_results.push({"test":test,
    625                                "subtests":subtests,
    626                                "status":status,
    627                                "message":message});
    628    },
    629 
    630    count: function() {
    631        return this.test_results.length;
    632    },
    633 
    634    to_json: function() {
    635        var test_results = this.test_results || [];
    636        var data = {
    637            "results": test_results.map(function(result) {
    638                var rv = {"test":(result.test.hasOwnProperty("ref_url") ?
    639                                  [result.test.url, result.test.ref_type, result.test.ref_url] :
    640                                  result.test.url),
    641                          "subtests":result.subtests,
    642                          "status":result.status,
    643                          "message":result.message};
    644                return rv;
    645            })
    646        };
    647        return JSON.stringify(data, null, 2);
    648    }
    649 };
    650 
    651 function Runner(manifest_path) {
    652    this.server = get_host_info().HTTP_ORIGIN;
    653    this.https_server = get_host_info().HTTPS_ORIGIN;
    654    this.manifest = new Manifest(manifest_path);
    655    this.path = null;
    656    this.test_types = null;
    657    this.manifest_iterator = null;
    658 
    659    this.test_window = null;
    660    this.test_div = document.getElementById('current_test');
    661    this.test_url = this.test_div.getElementsByTagName('a')[0];
    662    this.current_test = null;
    663    this.timeout = null;
    664    this.num_tests = null;
    665    this.pause_flag = false;
    666    this.stop_flag = false;
    667    this.done_flag = false;
    668 
    669    this.manifest_wait_callbacks = [];
    670    this.start_callbacks = [];
    671    this.test_start_callbacks = [];
    672    this.test_pause_callbacks = [];
    673    this.result_callbacks = [];
    674    this.done_callbacks = [];
    675 
    676    this.results = new Results(this);
    677 
    678    this.start_after_manifest_load = false;
    679    this.manifest_loading = true;
    680    this.manifest.load(this.manifest_loaded.bind(this));
    681 }
    682 
    683 Runner.prototype = {
    684    test_timeout: 20000, //ms
    685 
    686    currentTest: function() {
    687        return this.manifest[this.mTestCount];
    688    },
    689 
    690    ensure_test_window: function() {
    691        if (!this.test_window || this.test_window.location === null) {
    692          this.test_window = window.open("about:blank", 800, 600);
    693        }
    694    },
    695 
    696    manifest_loaded: function() {
    697        this.manifest_loading = false;
    698        if (this.start_after_manifest_load) {
    699            this.do_start();
    700        }
    701    },
    702 
    703    start: function(path, test_types, testharness_settings, use_regex) {
    704        this.pause_flag = false;
    705        this.stop_flag = false;
    706        this.done_flag = false;
    707        this.path = path;
    708        this.use_regex = use_regex;
    709        this.test_types = test_types;
    710        window.testharness_properties = testharness_settings;
    711        this.manifest_iterator = new ManifestIterator(this.manifest, this.path, this.test_types, this.use_regex);
    712        this.num_tests = null;
    713 
    714        this.ensure_test_window();
    715        if (this.manifest.data === null) {
    716            this.wait_for_manifest();
    717        } else {
    718            this.do_start();
    719        }
    720    },
    721 
    722    wait_for_manifest: function() {
    723        this.start_after_manifest_load = true;
    724        this.manifest_wait_callbacks.forEach(function(callback) {
    725            callback();
    726        });
    727    },
    728 
    729    do_start: function() {
    730        if (this.manifest_iterator.count() > 0) {
    731            this.start_callbacks.forEach(function(callback) {
    732                callback();
    733            });
    734            this.run_next_test();
    735        } else {
    736            var tests = "tests";
    737            if (this.test_types.length < 3) {
    738                tests = this.test_types.join(" tests or ") + " tests";
    739            }
    740            var message = "No " + tests + " found in this path."
    741            document.querySelector(".path").setCustomValidity(message);
    742            this.done();
    743        }
    744    },
    745 
    746    pause: function() {
    747        this.pause_flag = true;
    748        this.test_pause_callbacks.forEach(function(callback) {
    749            callback(this.current_test);
    750        }.bind(this));
    751    },
    752 
    753    unpause: function() {
    754        this.pause_flag = false;
    755        this.run_next_test();
    756    },
    757 
    758    on_result: function(status, message, subtests) {
    759        clearTimeout(this.timeout);
    760        this.results.set(this.current_test, status, message, subtests);
    761        this.result_callbacks.forEach(function(callback) {
    762            callback(this.current_test, status, message, subtests);
    763        }.bind(this));
    764        this.run_next_test();
    765    },
    766 
    767    on_timeout: function() {
    768        this.on_result("TIMEOUT", "", []);
    769    },
    770 
    771    done: function() {
    772        this.done_flag = true;
    773        if (this.test_window) {
    774            this.test_window.close();
    775            this.test_window = undefined;
    776        }
    777        this.done_callbacks.forEach(function(callback) {
    778            callback();
    779        });
    780    },
    781 
    782    run_next_test: function() {
    783        if (this.pause_flag) {
    784            return;
    785        }
    786        var next_test = this.manifest_iterator.next();
    787        if (next_test === null||this.done_flag) {
    788            this.done();
    789            return;
    790        }
    791 
    792        this.current_test = next_test;
    793 
    794        if (next_test.type === "testharness") {
    795            this.timeout = setTimeout(this.on_timeout.bind(this),
    796                                      this.test_timeout * window.testharness_properties.timeout_multiplier);
    797        }
    798        this.display_current_test(this.current_test.url);
    799        this.load(this.current_test.url);
    800 
    801        this.test_start_callbacks.forEach(function(callback) {
    802            callback(this.current_test);
    803        }.bind(this));
    804    },
    805 
    806    display_current_test: function(url) {
    807        var match_location, index, width;
    808 
    809        if (url === null) {
    810            this.test_div.style.visibility = "hidden";
    811            this.test_url.removeAttribute("href");
    812            this.test_url.textContent = "";
    813            return;
    814        }
    815 
    816        match_location = this.manifest_iterator.match_location(url);
    817        index = match_location.index;
    818        width = match_location.width;
    819 
    820        this.test_url.setAttribute("href", url);
    821        this.test_url.innerHTML = url.substring(0, index) +
    822            "<span class='match'>" +
    823            url.substring(index, index + width) +
    824            "</span>" +
    825            url.substring(index + width);
    826        this.test_div.style.visibility = "visible";
    827    },
    828 
    829    load: function(path) {
    830        this.ensure_test_window();
    831        if (path.match(/\.https\./))
    832          this.test_window.location.href = this.https_server + path;
    833        else
    834          this.test_window.location.href = this.server + path;
    835    },
    836 
    837    progress: function() {
    838        return this.results.count() / this.test_count();
    839    },
    840 
    841    test_count: function() {
    842        if (this.num_tests === null) {
    843            this.num_tests = this.manifest_iterator.count();
    844        }
    845        return this.num_tests;
    846    },
    847 
    848    on_complete: function(tests, status) {
    849      var harness_status_map = {0:"OK", 1:"ERROR", 2:"TIMEOUT", 3:"NOTRUN"};
    850      var subtest_status_map = {0:"PASS", 1:"FAIL", 2:"TIMEOUT", 3:"NOTRUN"};
    851 
    852      // this ugly hack is because IE really insists on holding on to the objects it creates in
    853      // other windows, and on losing track of them when the window gets closed
    854      var subtest_results = JSON.parse(JSON.stringify(
    855          tests.map(function (test) {
    856              return {name: test.name,
    857                      status: subtest_status_map[test.status],
    858                      message: test.message};
    859          })
    860      ));
    861 
    862      runner.on_result(harness_status_map[status.status],
    863                       status.message,
    864                       subtest_results);
    865    }
    866 };
    867 
    868 
    869 function parseOptions() {
    870    var options = {
    871        test_types: ["testharness", "reftest", "manual"]
    872    };
    873 
    874    var optionstrings = location.search.substring(1).split("&");
    875    for (var i = 0, il = optionstrings.length; i < il; ++i) {
    876        var opt = optionstrings[i];
    877        //TODO: fix this for complex-valued options
    878        options[opt.substring(0, opt.indexOf("="))] =
    879            opt.substring(opt.indexOf("=") + 1);
    880    }
    881    return options;
    882 }
    883 
    884 function setup() {
    885    var options = parseOptions();
    886 
    887    if (options.path) {
    888        document.getElementById('path').value = options.path;
    889    }
    890 
    891    runner = new Runner("/MANIFEST.json", options);
    892    var test_control = new TestControl(document.getElementById("testControl"), runner);
    893    new ManualUI(document.getElementById("manualUI"), runner);
    894    new VisualOutput(document.getElementById("output"), runner);
    895 
    896    window.addEventListener("message", function(e) {
    897        if (e.data.type === "complete")
    898            runner.on_complete(e.data.tests, e.data.status);
    899    });
    900 
    901    if (options.autorun === "1") {
    902        runner.start(test_control.get_path(),
    903                     test_control.get_test_types(),
    904                     test_control.get_testharness_settings(),
    905                     test_control.get_use_regex());
    906    }
    907 }
    908 
    909 window.addEventListener("DOMContentLoaded", setup, false);
    910 })();