Bug 1833753 [wpt PR 40065] - Allow newly-added test to also pass when mutation events...
[gecko.git] / testing / mochitest / server.js
blob6942fb0f75063fbde00c482fc841be22d22a1f30
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 // We expect these to be defined in the global scope by runtest.py.
8 /* global __LOCATION__, _PROFILE_PATH, _SERVER_PORT, _SERVER_ADDR, _DISPLAY_RESULTS,
9           _TEST_PREFIX */
10 // Defined by xpcshell
11 /* global quit */
13 /* import-globals-from ../../netwerk/test/httpserver/httpd.js */
14 /* eslint-disable mozilla/use-chromeutils-generateqi */
16 // Disable automatic network detection, so tests work correctly when
17 // not connected to a network.
18 // eslint-disable-next-line mozilla/use-services
19 var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
20 ios.manageOfflineStatus = false;
21 ios.offline = false;
23 var server; // for use in the shutdown handler, if necessary
26 // HTML GENERATION
28 /* global A, ABBR, ACRONYM, ADDRESS, APPLET, AREA, B, BASE,
29           BASEFONT, BDO, BIG, BLOCKQUOTE, BODY, BR, BUTTON,
30           CAPTION, CENTER, CITE, CODE, COL, COLGROUP, DD,
31           DEL, DFN, DIR, DIV, DL, DT, EM, FIELDSET, FONT,
32           FORM, FRAME, FRAMESET, H1, H2, H3, H4, H5, H6,
33           HEAD, HR, HTML, I, IFRAME, IMG, INPUT, INS,
34           ISINDEX, KBD, LABEL, LEGEND, LI, LINK, MAP, MENU,
35           META, NOFRAMES, NOSCRIPT, OBJECT, OL, OPTGROUP,
36           OPTION, P, PARAM, PRE, Q, S, SAMP, SCRIPT,
37           SELECT, SMALL, SPAN, STRIKE, STRONG, STYLE, SUB,
38           SUP, TABLE, TBODY, TD, TEXTAREA, TFOOT, TH, THEAD,
39           TITLE, TR, TT, U, UL, VAR */
40 var tags = [
41   "A",
42   "ABBR",
43   "ACRONYM",
44   "ADDRESS",
45   "APPLET",
46   "AREA",
47   "B",
48   "BASE",
49   "BASEFONT",
50   "BDO",
51   "BIG",
52   "BLOCKQUOTE",
53   "BODY",
54   "BR",
55   "BUTTON",
56   "CAPTION",
57   "CENTER",
58   "CITE",
59   "CODE",
60   "COL",
61   "COLGROUP",
62   "DD",
63   "DEL",
64   "DFN",
65   "DIR",
66   "DIV",
67   "DL",
68   "DT",
69   "EM",
70   "FIELDSET",
71   "FONT",
72   "FORM",
73   "FRAME",
74   "FRAMESET",
75   "H1",
76   "H2",
77   "H3",
78   "H4",
79   "H5",
80   "H6",
81   "HEAD",
82   "HR",
83   "HTML",
84   "I",
85   "IFRAME",
86   "IMG",
87   "INPUT",
88   "INS",
89   "ISINDEX",
90   "KBD",
91   "LABEL",
92   "LEGEND",
93   "LI",
94   "LINK",
95   "MAP",
96   "MENU",
97   "META",
98   "NOFRAMES",
99   "NOSCRIPT",
100   "OBJECT",
101   "OL",
102   "OPTGROUP",
103   "OPTION",
104   "P",
105   "PARAM",
106   "PRE",
107   "Q",
108   "S",
109   "SAMP",
110   "SCRIPT",
111   "SELECT",
112   "SMALL",
113   "SPAN",
114   "STRIKE",
115   "STRONG",
116   "STYLE",
117   "SUB",
118   "SUP",
119   "TABLE",
120   "TBODY",
121   "TD",
122   "TEXTAREA",
123   "TFOOT",
124   "TH",
125   "THEAD",
126   "TITLE",
127   "TR",
128   "TT",
129   "U",
130   "UL",
131   "VAR",
135  * Below, we'll use makeTagFunc to create a function for each of the
136  * strings in 'tags'. This will allow us to use s-expression like syntax
137  * to create HTML.
138  */
139 function makeTagFunc(tagName) {
140   return function (attrs /* rest... */) {
141     var startChildren = 0;
142     var response = "";
144     // write the start tag and attributes
145     response += "<" + tagName;
146     // if attr is an object, write attributes
147     if (attrs && typeof attrs == "object") {
148       startChildren = 1;
150       for (let key in attrs) {
151         const value = attrs[key];
152         var val = "" + value;
153         response += " " + key + '="' + val.replace('"', "&quot;") + '"';
154       }
155     }
156     response += ">";
158     // iterate through the rest of the args
159     for (var i = startChildren; i < arguments.length; i++) {
160       if (typeof arguments[i] == "function") {
161         response += arguments[i]();
162       } else {
163         response += arguments[i];
164       }
165     }
167     // write the close tag
168     response += "</" + tagName + ">\n";
169     return response;
170   };
173 function makeTags() {
174   // map our global HTML generation functions
175   for (let tag of tags) {
176     this[tag] = makeTagFunc(tag.toLowerCase());
177   }
180 var _quitting = false;
182 /** Quit when all activity has completed. */
183 function serverStopped() {
184   _quitting = true;
187 // only run the "main" section if httpd.js was loaded ahead of us
188 if (this.nsHttpServer) {
189   //
190   // SCRIPT CODE
191   //
192   runServer();
194   // We can only have gotten here if the /server/shutdown path was requested.
195   if (_quitting) {
196     dumpn("HTTP server stopped, all pending requests complete");
197     quit(0);
198   }
200   // Impossible as the stop callback should have been called, but to be safe...
201   dumpn("TEST-UNEXPECTED-FAIL | failure to correctly shut down HTTP server");
202   quit(1);
205 var serverBasePath;
206 var displayResults = true;
208 var gServerAddress;
209 var SERVER_PORT;
212 // SERVER SETUP
214 function runServer() {
215   serverBasePath = __LOCATION__.parent;
216   server = createMochitestServer(serverBasePath);
218   // verify server address
219   // if a.b.c.d or 'localhost'
220   if (typeof _SERVER_ADDR != "undefined") {
221     if (_SERVER_ADDR == "localhost") {
222       gServerAddress = _SERVER_ADDR;
223     } else {
224       var quads = _SERVER_ADDR.split(".");
225       if (quads.length == 4) {
226         var invalid = false;
227         for (var i = 0; i < 4; i++) {
228           if (quads[i] < 0 || quads[i] > 255) {
229             invalid = true;
230           }
231         }
232         if (!invalid) {
233           gServerAddress = _SERVER_ADDR;
234         } else {
235           throw new Error(
236             "invalid _SERVER_ADDR, please specify a valid IP Address"
237           );
238         }
239       }
240     }
241   } else {
242     throw new Error(
243       "please define _SERVER_ADDR (as an ip address) before running server.js"
244     );
245   }
247   if (typeof _SERVER_PORT != "undefined") {
248     if (parseInt(_SERVER_PORT) > 0 && parseInt(_SERVER_PORT) < 65536) {
249       SERVER_PORT = _SERVER_PORT;
250     }
251   } else {
252     throw new Error(
253       "please define _SERVER_PORT (as a port number) before running server.js"
254     );
255   }
257   // If DISPLAY_RESULTS is not specified, it defaults to true
258   if (typeof _DISPLAY_RESULTS != "undefined") {
259     displayResults = _DISPLAY_RESULTS;
260   }
262   server._start(SERVER_PORT, gServerAddress);
264   // touch a file in the profile directory to indicate we're alive
265   var foStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
266     Ci.nsIFileOutputStream
267   );
268   var serverAlive = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
270   if (typeof _PROFILE_PATH == "undefined") {
271     serverAlive.initWithFile(serverBasePath);
272     serverAlive.append("mochitesttestingprofile");
273   } else {
274     serverAlive.initWithPath(_PROFILE_PATH);
275   }
277   // Create a file to inform the harness that the server is ready
278   if (serverAlive.exists()) {
279     serverAlive.append("server_alive.txt");
280     foStream.init(serverAlive, 0x02 | 0x08 | 0x20, 436, 0); // write, create, truncate
281     var data = "It's alive!";
282     foStream.write(data, data.length);
283     foStream.close();
284   } else {
285     throw new Error(
286       "Failed to create server_alive.txt because " +
287         serverAlive.path +
288         " could not be found."
289     );
290   }
292   makeTags();
294   //
295   // The following is threading magic to spin an event loop -- this has to
296   // happen manually in xpcshell for the server to actually work.
297   //
298   var thread = Cc["@mozilla.org/thread-manager;1"].getService().currentThread;
299   while (!server.isStopped()) {
300     thread.processNextEvent(true);
301   }
303   // Server stopped by /server/shutdown handler -- go through pending events
304   // and return.
306   // get rid of any pending requests
307   while (thread.hasPendingEvents()) {
308     thread.processNextEvent(true);
309   }
312 /** Creates and returns an HTTP server configured to serve Mochitests. */
313 function createMochitestServer(serverBasePath) {
314   var server = new nsHttpServer();
316   server.registerDirectory("/", serverBasePath);
317   server.registerPathHandler("/server/shutdown", serverShutdown);
318   server.registerPathHandler("/server/debug", serverDebug);
319   server.registerContentType("sjs", "sjs"); // .sjs == CGI-like functionality
320   server.registerContentType("jar", "application/x-jar");
321   server.registerContentType("ogg", "application/ogg");
322   server.registerContentType("pdf", "application/pdf");
323   server.registerContentType("ogv", "video/ogg");
324   server.registerContentType("oga", "audio/ogg");
325   server.registerContentType("opus", "audio/ogg; codecs=opus");
326   server.registerContentType("dat", "text/plain; charset=utf-8");
327   server.registerContentType("frag", "text/plain"); // .frag == WebGL fragment shader
328   server.registerContentType("vert", "text/plain"); // .vert == WebGL vertex shader
329   server.registerContentType("wasm", "application/wasm");
330   server.setIndexHandler(defaultDirHandler);
332   var serverRoot = {
333     getFile: function getFile(path) {
334       var file = serverBasePath.clone().QueryInterface(Ci.nsIFile);
335       path.split("/").forEach(function (p) {
336         file.appendRelativePath(p);
337       });
338       return file;
339     },
340     QueryInterface(aIID) {
341       return this;
342     },
343   };
345   server.setObjectState("SERVER_ROOT", serverRoot);
347   processLocations(server);
349   return server;
353  * Notifies the HTTP server about all the locations at which it might receive
354  * requests, so that it can properly respond to requests on any of the hosts it
355  * serves.
356  */
357 function processLocations(server) {
358   var serverLocations = serverBasePath.clone();
359   serverLocations.append("server-locations.txt");
361   const PR_RDONLY = 0x01;
362   var fis = new FileInputStream(
363     serverLocations,
364     PR_RDONLY,
365     292 /* 0444 */,
366     Ci.nsIFileInputStream.CLOSE_ON_EOF
367   );
369   var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
370   lis.QueryInterface(Ci.nsIUnicharLineInputStream);
372   const LINE_REGEXP = new RegExp(
373     "^([a-z][-a-z0-9+.]*)" +
374       "://" +
375       "(" +
376       "\\d+\\.\\d+\\.\\d+\\.\\d+" +
377       "|" +
378       "(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\\.)*" +
379       "[a-z](?:[-a-z0-9]*[a-z0-9])?" +
380       ")" +
381       ":" +
382       "(\\d+)" +
383       "(?:" +
384       "\\s+" +
385       "(\\S+(?:,\\S+)*)" +
386       ")?$"
387   );
389   var line = {};
390   var lineno = 0;
391   var seenPrimary = false;
392   do {
393     var more = lis.readLine(line);
394     lineno++;
396     var lineValue = line.value;
397     if (lineValue.charAt(0) == "#" || lineValue == "") {
398       continue;
399     }
401     var match = LINE_REGEXP.exec(lineValue);
402     if (!match) {
403       throw new Error("Syntax error in server-locations.txt, line " + lineno);
404     }
406     var [, scheme, host, port, options] = match;
407     if (options) {
408       if (options.split(",").includes("primary")) {
409         if (seenPrimary) {
410           throw new Error(
411             "Multiple primary locations in server-locations.txt, " +
412               "line " +
413               lineno
414           );
415         }
417         server.identity.setPrimary(scheme, host, port);
418         seenPrimary = true;
419         continue;
420       }
421     }
423     server.identity.add(scheme, host, port);
424   } while (more);
427 // PATH HANDLERS
429 // /server/shutdown
430 function serverShutdown(metadata, response) {
431   response.setStatusLine("1.1", 200, "OK");
432   response.setHeader("Content-type", "text/plain", false);
434   var body = "Server shut down.";
435   response.bodyOutputStream.write(body, body.length);
437   dumpn("Server shutting down now...");
438   server.stop(serverStopped);
441 // /server/debug?[012]
442 function serverDebug(metadata, response) {
443   response.setStatusLine(metadata.httpVersion, 400, "Bad debugging level");
444   if (metadata.queryString.length !== 1) {
445     return;
446   }
448   var mode;
449   if (metadata.queryString === "0") {
450     // do this now so it gets logged with the old mode
451     dumpn("Server debug logs disabled.");
452     DEBUG = false;
453     DEBUG_TIMESTAMP = false;
454     mode = "disabled";
455   } else if (metadata.queryString === "1") {
456     DEBUG = true;
457     DEBUG_TIMESTAMP = false;
458     mode = "enabled";
459   } else if (metadata.queryString === "2") {
460     DEBUG = true;
461     DEBUG_TIMESTAMP = true;
462     mode = "enabled, with timestamps";
463   } else {
464     return;
465   }
467   response.setStatusLine(metadata.httpVersion, 200, "OK");
468   response.setHeader("Content-type", "text/plain", false);
469   var body = "Server debug logs " + mode + ".";
470   response.bodyOutputStream.write(body, body.length);
471   dumpn(body);
475 // DIRECTORY LISTINGS
479  * Creates a generator that iterates over the contents of
480  * an nsIFile directory.
481  */
482 function* dirIter(dir) {
483   var en = dir.directoryEntries;
484   while (en.hasMoreElements()) {
485     yield en.nextFile;
486   }
490  * Builds an optionally nested object containing links to the
491  * files and directories within dir.
492  */
493 function list(requestPath, directory, recurse) {
494   var count = 0;
495   var path = requestPath;
496   if (path.charAt(path.length - 1) != "/") {
497     path += "/";
498   }
500   var dir = directory.QueryInterface(Ci.nsIFile);
501   var links = {};
503   // The SimpleTest directory is hidden
504   let files = [];
505   for (let file of dirIter(dir)) {
506     if (file.exists() && !file.path.includes("SimpleTest")) {
507       files.push(file);
508     }
509   }
511   // Sort files by name, so that tests can be run in a pre-defined order inside
512   // a given directory (see bug 384823)
513   function leafNameComparator(first, second) {
514     if (first.leafName < second.leafName) {
515       return -1;
516     }
517     if (first.leafName > second.leafName) {
518       return 1;
519     }
520     return 0;
521   }
522   files.sort(leafNameComparator);
524   count = files.length;
525   for (let file of files) {
526     var key = path + file.leafName;
527     var childCount = 0;
528     if (file.isDirectory()) {
529       key += "/";
530     }
531     if (recurse && file.isDirectory()) {
532       [links[key], childCount] = list(key, file, recurse);
533       count += childCount;
534     } else if (file.leafName.charAt(0) != ".") {
535       links[key] = { test: { url: key, expected: "pass" } };
536     }
537   }
539   return [links, count];
543  * Heuristic function that determines whether a given path
544  * is a test case to be executed in the harness, or just
545  * a supporting file.
546  */
547 function isTest(filename, pattern) {
548   if (pattern) {
549     return pattern.test(filename);
550   }
552   // File name is a URL style path to a test file, make sure that we check for
553   // tests that start with the appropriate prefix.
554   var testPrefix = typeof _TEST_PREFIX == "string" ? _TEST_PREFIX : "test_";
555   var testPattern = new RegExp("^" + testPrefix);
557   var pathPieces = filename.split("/");
559   return (
560     testPattern.test(pathPieces[pathPieces.length - 1]) &&
561     !filename.includes(".js") &&
562     !filename.includes(".css") &&
563     !/\^headers\^$/.test(filename)
564   );
568  * Transform nested hashtables of paths to nested HTML lists.
569  */
570 function linksToListItems(links) {
571   var response = "";
572   var children = "";
573   for (let link in links) {
574     const value = links[link];
575     var classVal =
576       !isTest(link) && !(value instanceof Object)
577         ? "non-test invisible"
578         : "test";
579     if (value instanceof Object) {
580       children = UL({ class: "testdir" }, linksToListItems(value));
581     } else {
582       children = "";
583     }
585     var bug_title = link.match(/test_bug\S+/);
586     var bug_num = null;
587     if (bug_title != null) {
588       bug_num = bug_title[0].match(/\d+/);
589     }
591     if (bug_title == null || bug_num == null) {
592       response += LI({ class: classVal }, A({ href: link }, link), children);
593     } else {
594       var bug_url = "https://bugzilla.mozilla.org/show_bug.cgi?id=" + bug_num;
595       response += LI(
596         { class: classVal },
597         A({ href: link }, link),
598         " - ",
599         A({ href: bug_url }, "Bug " + bug_num),
600         children
601       );
602     }
603   }
604   return response;
608  * Transform nested hashtables of paths to a flat table rows.
609  */
610 function linksToTableRows(links, recursionLevel) {
611   var response = "";
612   for (let link in links) {
613     const value = links[link];
614     var classVal =
615       !isTest(link) && value instanceof Object && "test" in value
616         ? "non-test invisible"
617         : "";
619     var spacer = "padding-left: " + 10 * recursionLevel + "px";
621     if (value instanceof Object && !("test" in value)) {
622       response += TR(
623         { class: "dir", id: "tr-" + link },
624         TD({ colspan: "3" }, "&#160;"),
625         TD({ style: spacer }, A({ href: link }, link))
626       );
627       response += linksToTableRows(value, recursionLevel + 1);
628     } else {
629       var bug_title = link.match(/test_bug\S+/);
630       var bug_num = null;
631       if (bug_title != null) {
632         bug_num = bug_title[0].match(/\d+/);
633       }
634       if (bug_title == null || bug_num == null) {
635         response += TR(
636           { class: classVal, id: "tr-" + link },
637           TD("0"),
638           TD("0"),
639           TD("0"),
640           TD({ style: spacer }, A({ href: link }, link))
641         );
642       } else {
643         var bug_url = "https://bugzilla.mozilla.org/show_bug.cgi?id=" + bug_num;
644         response += TR(
645           { class: classVal, id: "tr-" + link },
646           TD("0"),
647           TD("0"),
648           TD("0"),
649           TD(
650             { style: spacer },
651             A({ href: link }, link),
652             " - ",
653             A({ href: bug_url }, "Bug " + bug_num)
654           )
655         );
656       }
657     }
658   }
659   return response;
662 function arrayOfTestFiles(linkArray, fileArray, testPattern) {
663   for (let link in linkArray) {
664     const value = linkArray[link];
665     if (value instanceof Object && !("test" in value)) {
666       arrayOfTestFiles(value, fileArray, testPattern);
667     } else if (isTest(link, testPattern) && value instanceof Object) {
668       fileArray.push(value.test);
669     }
670   }
673  * Produce a flat array of test file paths to be executed in the harness.
674  */
675 function jsonArrayOfTestFiles(links) {
676   var testFiles = [];
677   arrayOfTestFiles(links, testFiles);
678   testFiles = testFiles.map(function (file) {
679     return '"' + file.url + '"';
680   });
682   return "[" + testFiles.join(",\n") + "]";
686  * Produce a normal directory listing.
687  */
688 function regularListing(metadata, response) {
689   var [links] = list(metadata.path, metadata.getProperty("directory"), false);
690   response.write(
691     HTML(
692       HEAD(TITLE("mochitest index ", metadata.path)),
693       BODY(BR(), A({ href: ".." }, "Up a level"), UL(linksToListItems(links)))
694     )
695   );
699  * Read a manifestFile located at the root of the server's directory and turn
700  * it into an object for creating a table of clickable links for each test.
701  */
702 function convertManifestToTestLinks(root, manifest) {
703   const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
705   var manifestFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
706   manifestFile.initWithFile(serverBasePath);
707   manifestFile.append(manifest);
709   var manifestStream = Cc[
710     "@mozilla.org/network/file-input-stream;1"
711   ].createInstance(Ci.nsIFileInputStream);
712   manifestStream.init(manifestFile, -1, 0, 0);
714   var manifestObj = JSON.parse(
715     NetUtil.readInputStreamToString(manifestStream, manifestStream.available())
716   );
717   var paths = manifestObj.tests;
718   var pathPrefix = "/" + root + "/";
719   return [
720     paths.reduce(function (t, p) {
721       t[pathPrefix + p.path] = true;
722       return t;
723     }, {}),
724     paths.length,
725   ];
729  * Produce a test harness page containing all the test cases
730  * below it, recursively.
731  */
732 function testListing(metadata, response) {
733   var links = {};
734   var count = 0;
735   if (!metadata.queryString.includes("manifestFile")) {
736     [links, count] = list(
737       metadata.path,
738       metadata.getProperty("directory"),
739       true
740     );
741   } else if (typeof Components != "undefined") {
742     var manifest = metadata.queryString.match(/manifestFile=([^&]+)/)[1];
744     [links, count] = convertManifestToTestLinks(
745       metadata.path.split("/")[1],
746       manifest
747     );
748   }
750   var table_class =
751     metadata.queryString.indexOf("hideResultsTable=1") > -1 ? "invisible" : "";
753   let testname =
754     metadata.queryString.indexOf("testname=") > -1
755       ? metadata.queryString.match(/testname=([^&]+)/)[1]
756       : "";
758   dumpn("count: " + count);
759   var tests = testname ? "['/" + testname + "']" : jsonArrayOfTestFiles(links);
760   response.write(
761     HTML(
762       HEAD(
763         TITLE("MochiTest | ", metadata.path),
764         LINK({
765           rel: "stylesheet",
766           type: "text/css",
767           href: "/static/harness.css",
768         }),
769         SCRIPT({
770           type: "text/javascript",
771           src: "/tests/SimpleTest/LogController.js",
772         }),
773         SCRIPT({
774           type: "text/javascript",
775           src: "/tests/SimpleTest/MemoryStats.js",
776         }),
777         SCRIPT({
778           type: "text/javascript",
779           src: "/tests/SimpleTest/TestRunner.js",
780         }),
781         SCRIPT({
782           type: "text/javascript",
783           src: "/tests/SimpleTest/MozillaLogger.js",
784         }),
785         SCRIPT({ type: "text/javascript", src: "/chunkifyTests.js" }),
786         SCRIPT({ type: "text/javascript", src: "/manifestLibrary.js" }),
787         SCRIPT({ type: "text/javascript", src: "/tests/SimpleTest/setup.js" }),
788         SCRIPT(
789           { type: "text/javascript" },
790           "window.onload =  hookup; gTestList=" + tests + ";"
791         )
792       ),
793       BODY(
794         DIV(
795           { class: "container" },
796           H2("--> ", A({ href: "#", id: "runtests" }, "Run Tests"), " <--"),
797           P(
798             { style: "float: right;" },
799             SMALL(
800               "Based on the ",
801               A({ href: "http://www.mochikit.com/" }, "MochiKit"),
802               " unit tests."
803             )
804           ),
805           DIV(
806             { class: "status" },
807             H1({ id: "indicator" }, "Status"),
808             H2({ id: "pass" }, "Passed: ", SPAN({ id: "pass-count" }, "0")),
809             H2({ id: "fail" }, "Failed: ", SPAN({ id: "fail-count" }, "0")),
810             H2({ id: "fail" }, "Todo: ", SPAN({ id: "todo-count" }, "0"))
811           ),
812           DIV({ class: "clear" }),
813           DIV(
814             { id: "current-test" },
815             B("Currently Executing: ", SPAN({ id: "current-test-path" }, "_"))
816           ),
817           DIV({ class: "clear" }),
818           DIV(
819             { class: "frameholder" },
820             IFRAME({ scrolling: "no", id: "testframe", allowfullscreen: true })
821           ),
822           DIV({ class: "clear" }),
823           DIV(
824             { class: "toggle" },
825             A({ href: "#", id: "toggleNonTests" }, "Show Non-Tests"),
826             BR()
827           ),
829           displayResults
830             ? TABLE(
831                 {
832                   cellpadding: 0,
833                   cellspacing: 0,
834                   class: table_class,
835                   id: "test-table",
836                 },
837                 TR(TD("Passed"), TD("Failed"), TD("Todo"), TD("Test Files")),
838                 linksToTableRows(links, 0)
839               )
840             : "",
841           BR(),
842           TABLE({
843             cellpadding: 0,
844             cellspacing: 0,
845             border: 1,
846             bordercolor: "red",
847             id: "fail-table",
848           }),
850           DIV({ class: "clear" })
851         )
852       )
853     )
854   );
858  * Respond to requests that match a file system directory.
859  * Under the tests/ directory, return a test harness page.
860  */
861 function defaultDirHandler(metadata, response) {
862   response.setStatusLine("1.1", 200, "OK");
863   response.setHeader("Content-type", "text/html;charset=utf-8", false);
864   try {
865     if (metadata.path.indexOf("/tests") != 0) {
866       regularListing(metadata, response);
867     } else {
868       testListing(metadata, response);
869     }
870   } catch (ex) {
871     response.write(ex);
872   }