tor-browser

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

configuration.html (55443B)


      1 <!DOCTYPE html>
      2 <html lang="en">
      3  <head>
      4    <meta charset="UTF-8" />
      5    <meta name="viewport" content="width=device-width, initial-scale=1" />
      6    <title>Session Configuration - Web Platform Test</title>
      7    <link rel="stylesheet" href="css/bulma-0.7.5/bulma.min.css" />
      8    <link rel="stylesheet" href="css/fontawesome-5.7.2.min.css" />
      9    <script src="lib/utils.js"></script>
     10    <script src="lib/wave-service.js"></script>
     11    <script src="lib/ui.js"></script>
     12    <style>
     13      .site-logo {
     14        max-width: 300px;
     15        margin: 0 0 30px -15px;
     16      }
     17    </style>
     18  </head>
     19  <body>
     20    <script>
     21      //      var apis = [
     22      //        { title: "2D Context", path: "/2dcontext" },
     23      //        { title: "Content Security Policy", path: "/content-security-policy" },
     24      //        { title: "CSS", path: "/css" },
     25      //        { title: "DOM", path: "/dom" },
     26      //        { title: "ECMAScript", path: "/ecmascript" },
     27      //        { title: "Encrypted media", path: "/encrypted-media" },
     28      //        { title: "Fetch", path: "/fetch" },
     29      //        { title: "FileAPI", path: "/FileAPI" },
     30      //        { title: "Fullscreen", path: "/fullscreen" },
     31      //        { title: "WebGL", path: "/webgl" },
     32      //        { title: "HTML", path: "/html" },
     33      //        { title: "IndexedDB", path: "/IndexedDB" },
     34      //        { title: "Media Source", path: "/media-source" },
     35      //        { title: "Notifications", path: "/notifications" },
     36      //        { title: "Page Visibility", path: "/page-visibility" },
     37      //        { title: "Service Workers", path: "/service-workers" },
     38      //        { title: "UI Events", path: "/uievents" },
     39      //        { title: "WAVE Extra", path: "/wave-extra" },
     40      //        { title: "Webaudio", path: "/webaudio" },
     41      //        { title: "WebCryptoAPI", path: "/WebCryptoAPI" },
     42      //        { title: "Webmessaging", path: "/webmessaging" },
     43      //        { title: "Websockets", path: "/websockets" },
     44      //        { title: "Webstorage", path: "/webstorage" },
     45      //        { title: "Workers", path: "/workers" },
     46      //        { title: "XHR", path: "/xhr" }
     47      //      ];
     48 
     49      //      var referenceSessions = [
     50      //        {
     51      //          title: "Edge 44.17763",
     52      //          engine: "",
     53      //          token: "b2924d20-6a93-11e9-98b4-a11fb92a6d1c",
     54      //          icon: "fab fa-edge"
     55      //        },
     56      //        {
     57      //          title: "Firefox 64.0",
     58      //          engine: "Gecko 64.0",
     59      //          token: "bb7aafa0-6a92-11e9-8ec2-04f58dad2e4f",
     60      //          icon: "fab fa-firefox"
     61      //        },
     62      //        {
     63      //          title: "WebKit 605.1.15",
     64      //          engine: "Revision 239158",
     65      //          token: "caf823e0-6a92-11e9-b732-3188d0065ebc",
     66      //          icon: "fab fa-safari"
     67      //        },
     68      //        {
     69      //          title: "Chromium 73.0.3640.0",
     70      //          engine: "Blink 537.36",
     71      //          token: "a50c6db0-6a94-11e9-8d1b-e23fc4555885",
     72      //          icon: "fab fa-chrome"
     73      //        }
     74      //      ];
     75 
     76      var testFileSelectionEnabled = true;
     77 
     78      window.onload = function () {
     79        new ConfigurationView();
     80      };
     81 
     82      function ConfigurationView() {
     83        const query = utils.parseQuery(location.search);
     84        var token = query.token;
     85        if (token) WaveService.addRecentSession(token);
     86        var referenceSessions = [];
     87        var apis = [];
     88        var testTypeSelectionEnabled = true;
     89        var testFileSelectionEnabled = false;
     90 
     91        var types = [
     92          { title: "Automatic", value: "automatic" },
     93          { title: "Manual", value: "manual" },
     94        ];
     95        var state = {};
     96        loadServerStatus();
     97        loadSessionData(token, function () {
     98                loadPublicSessionData(function () {
     99                  renderReferencesField();
    100                });
    101        });
    102        render();
    103 
    104        function loadServerStatus() {
    105          WaveService.readStatus(function (status) {
    106            testTypeSelectionEnabled = status.testTypeSelectionEnabled;
    107            testFileSelectionEnabled = status.testFileSelectionEnabled;
    108            renderSessionConfiguration();
    109          });
    110        }
    111 
    112        function loadSessionData(token, callback) {
    113          if (!token) {
    114            state.expired = true;
    115            return;
    116          }
    117          WaveService.readSessionStatus(
    118            token,
    119            function (status) {
    120              if (status.status !== "pending") {
    121                openResultsPage(token);
    122                return;
    123              }
    124              state.status = status;
    125 
    126              WaveService.readSession(token, function (configuration) {
    127                if (
    128                  configuration.tests.include.findIndex(
    129                    (test) => test === "/"
    130                  ) !== -1
    131                ) {
    132                  configuration.tests.include = apis.map((api) => api.path);
    133                }
    134                state.configurationBackup = utils.copyObject(configuration);
    135                state.configuration = configuration;
    136                setTimeout(
    137                  handleExpiration,
    138                  status.expirationDate.getTime() - Date.now()
    139                );
    140                renderSessionConfiguration();
    141                callback();
    142                WaveService.readAvailableApis(function (available_apis) {
    143                  available_apis = available_apis.sort((apiA, apiB) =>
    144                    apiA.title.toLowerCase() > apiB.title.toLowerCase() ? 1 : -1
    145                  );
    146                  apis = available_apis;
    147                  selectAllTests();
    148                  renderSessionConfiguration();
    149                });
    150              });
    151            },
    152            function () {
    153              state.expired = true;
    154              renderSessionConfiguration();
    155              renderExcludedTests();
    156              renderResumeView();
    157            }
    158          );
    159        }
    160 
    161        function loadPublicSessionData(callback) {
    162          WaveService.readPublicSessions(function (tokens) {
    163            WaveService.readMultipleSessions(tokens, function (configurations) {
    164                    console.log(configurations);
    165              referenceSessions = configurations
    166                .sort((confA, confB) =>
    167                  confA.browser.name.toLowerCase() >
    168                  confB.browser.name.toLowerCase()
    169                    ? 1
    170                    : -1
    171                )
    172                .map((configuration) => {
    173                  var icon = "";
    174                  switch (configuration.browser.name.toLowerCase()) {
    175                    case "firefox":
    176                      icon = "fab fa-firefox";
    177                      break;
    178                    case "webkit":
    179                    case "safari":
    180                      icon = "fab fa-safari";
    181                      break;
    182                    case "edge":
    183                      icon = "fab fa-edge";
    184                      break;
    185                    case "chrome":
    186                    case "chromium":
    187                      icon = "fab fa-chrome";
    188                      break;
    189                  }
    190                  return {
    191                    title:
    192                      configuration.browser.name +
    193                      " " +
    194                      configuration.browser.version,
    195                    token: configuration.token,
    196                    icon,
    197                    engine: configuration.browser.engine || "",
    198                  };
    199                });
    200              callback(referenceSessions);
    201            });
    202          });
    203        }
    204 
    205        function handleConfigureSession() {
    206          const tokenFragmentInput = UI.getElement("token-fragment");
    207          const fragment = tokenFragmentInput.value;
    208          findSession(fragment, function (session) {
    209            if (!session) {
    210              const errorBox = UI.getElement("find-error");
    211              errorBox.setAttribute("style", "display: block");
    212              return;
    213            }
    214            tokenFragmentInput.value = "";
    215            const errorBox = UI.getElement("find-error");
    216            errorBox.setAttribute("style", "display: none");
    217            const path = location.pathname + "?token=" + session.token;
    218            location.href = path;
    219          });
    220        }
    221 
    222        function findSession(fragment, callback) {
    223          if (!fragment || fragment.length < 8) {
    224            callback(null);
    225            return;
    226          }
    227          WaveService.findToken(
    228            fragment,
    229            function (token) {
    230              WaveService.readSession(token, function (session) {
    231                WaveService.readSessionStatus(token, function (status) {
    232                  session.status = status.status;
    233                  session.dateStarted = status.dateStarted;
    234                  session.dateFinished = status.dateFinished;
    235                  callback(session);
    236                });
    237              });
    238            },
    239            function () {
    240              callback(null);
    241            }
    242          );
    243        }
    244 
    245        function hasIncludedTest(path) {
    246          var tests = state.configuration.tests;
    247          //var index = tests.include.findIndex(function (test) {
    248          //  return test.match(new RegExp("^" + path));
    249          //});
    250          var index = tests.include.indexOf(path);
    251          return index !== -1;
    252        }
    253 
    254        function handleIncludedTestToggle(path) {
    255          var configuration = state.configuration;
    256          if (hasIncludedTest(path)) {
    257            handleRemoveIncludedTest(path);
    258          } else {
    259            handleAddIncludedTest(path);
    260          }
    261        }
    262 
    263        function handleAddIncludedTest(path) {
    264          var tests = state.configuration.tests;
    265          if (state.tests && state.tests[path.substr(1)]) {
    266            tests.include = tests.include.filter(function (test) {
    267              return !test.match(new RegExp("^" + path + "/"));
    268            });
    269            tests.include = tests.include.concat(state.tests[path.substr(1)]);
    270          } else {
    271            tests.include.push(path);
    272          }
    273        }
    274 
    275        function handleRemoveIncludedTest(path) {
    276          var tests = state.configuration.tests;
    277 
    278          if (state.tests && state.tests[path.substr(1)]) {
    279            tests.include = tests.include.filter(function (test) {
    280              return !test.match(new RegExp("^" + path + "/"));
    281            });
    282          } else {
    283            var index = tests.include.findIndex((test) => test === path);
    284            tests.include.splice(index, 1);
    285          }
    286        }
    287 
    288        function getIncludedRatio(path) {
    289          var includedTests = state.configuration.tests.include;
    290          if (!state.tests) {
    291            return includedTests.indexOf(path) !== -1 ? 1 : 0;
    292          }
    293          var count = 0;
    294          for (var test of includedTests) {
    295            if (!test.match(new RegExp("^" + path))) continue;
    296            count++;
    297          }
    298          return count / state.tests[path.substr(1)].length;
    299        }
    300 
    301        function selectAllTests() {
    302          var tests = state.configuration.tests;
    303          if (state.tests) {
    304            tests.include = [];
    305            for (var api in state.tests) {
    306              tests.include = tests.include.concat(state.tests[api]);
    307            }
    308          } else {
    309            tests.include = apis.map((api) => api.path);
    310          }
    311        }
    312 
    313        function deselectAllTests() {
    314          var configuration = state.configuration;
    315          configuration.tests.include = [];
    316        }
    317 
    318        function hasTestType(value) {
    319          var configuration = state.configuration;
    320          var index = configuration.types.findIndex((type) => type === value);
    321          return index !== -1;
    322        }
    323 
    324        function handleTestTypeToggle(value) {
    325          var configuration = state.configuration;
    326          if (hasTestType(value)) {
    327            var index = configuration.types.findIndex((type) => type === value);
    328            configuration.types.splice(index, 1);
    329          } else {
    330            configuration.types.push(value);
    331          }
    332        }
    333 
    334        function selectAllTestTypes() {
    335          var configuration = state.configuration;
    336          configuration.types = types.map((type) => type.value);
    337        }
    338 
    339        function deselectAllTestTypes() {
    340          var configuration = state.configuration;
    341          configuration.types = [];
    342        }
    343 
    344        function hasRefSession(session) {
    345          var configuration = state.configuration;
    346          var index = configuration.referenceTokens.findIndex(
    347            (token) => token === session.token
    348          );
    349          return index !== -1;
    350        }
    351 
    352        function handleRefSessionToggle(session) {
    353          var configuration = state.configuration;
    354          if (hasRefSession(session)) {
    355            var index = configuration.referenceTokens.findIndex(
    356              (token) => token === session.token
    357            );
    358            configuration.referenceTokens.splice(index, 1);
    359          } else {
    360            configuration.referenceTokens.push(session.token);
    361          }
    362        }
    363 
    364        function selectAllRefSessions() {
    365          var configuration = state.configuration;
    366          configuration.referenceTokens = referenceSessions.map(
    367            (session) => session.token
    368          );
    369        }
    370 
    371        function deselectAllRefSessions() {
    372          var configuration = state.configuration;
    373          configuration.referenceTokens = [];
    374        }
    375 
    376        function isTestListValid() {
    377          var configuration = state.configuration;
    378          return configuration.tests.include.length > 0;
    379        }
    380 
    381        function isTestTypeListValid() {
    382          var configuration = state.configuration;
    383          return configuration.types.length > 0;
    384        }
    385 
    386        function isConfigurationValid() {
    387          if (!isTestListValid()) return false;
    388          if (!isTestTypeListValid()) return false;
    389          return true;
    390        }
    391 
    392        function isSessionStarting() {
    393          return state.isStarting;
    394        }
    395 
    396        function checkApiList() {
    397          var apiErrorElement = UI.getElement("api-error");
    398          apiErrorElement.innerHTML = "";
    399          if (!isTestListValid()) {
    400            apiErrorElement.appendChild(
    401              UI.createElement(
    402                createErrorMessage(
    403                  "Select at least one API or at least one test within an API"
    404                )
    405              )
    406            );
    407          }
    408          renderButtonsField();
    409        }
    410 
    411        function handleStart() {
    412          if (isSessionStarting()) return;
    413          var configuration = state.configuration;
    414          var token = configuration.token;
    415          WaveService.updateSession(token, configuration, function () {
    416            WaveService.updateLabels(token, configuration.labels, function () {
    417              WaveService.startSession(token, function () {
    418                openResultsPage(token);
    419              });
    420            });
    421          });
    422          state.isStarting = true;
    423        }
    424 
    425        function handleDiscardChanges() {
    426          state.configuration = utils.copyObject(state.configurationBackup);
    427        }
    428 
    429        function handleExpiration() {
    430          state.expired = true;
    431          renderSessionConfiguration();
    432          renderResumeView();
    433        }
    434 
    435        function openResultsPage(token) {
    436          location.href = WEB_ROOT + "results.html?token=" + token;
    437        }
    438 
    439        function handleAddExludedTestsRaw() {
    440          var excludedTestsTextArea = UI.getElement("excluded-tests-text");
    441          var tests = excludedTestsTextArea.value.split("\n");
    442          var configuration = state.configuration;
    443          var excludedTests = configuration.tests.exclude;
    444          for (var test of tests) {
    445            if (!test) continue;
    446            if (test.startsWith("#")) continue;
    447            if (excludedTests.indexOf(test) !== -1) continue;
    448            excludedTests.push(test);
    449          }
    450 
    451          excludedTestsTextArea.value = "";
    452          renderExcludedTests();
    453        }
    454 
    455        function handleAddExludedTestsMalfunctioning() {
    456          var excludedTestsTextArea = UI.getElement("excluded-tests-text");
    457          var token = excludedTestsTextArea.value;
    458          var tokenRegExp = new RegExp(
    459            "^[a-f0-9]{8}(-[a-f0-9]{0,4}|$)(-[a-f0-9]{0,4}|$)(-[a-f0-9]{0,4}|$)(-[a-f0-9]{0,12}|$)"
    460          );
    461          var configuration = state.configuration;
    462          var excludedTests = configuration.tests.exclude;
    463          if (tokenRegExp.test(token)) {
    464            WaveService.findToken(
    465              token,
    466              function (token) {
    467                WaveService.readMalfunctioningTests(token, function (
    468                  malfunctioningTests
    469                ) {
    470                  for (var test of malfunctioningTests) {
    471                    if (!test) continue;
    472                    if (excludedTests.indexOf(test) !== -1) continue;
    473                    excludedTests.push(test);
    474                  }
    475                  renderExcludedTests();
    476                });
    477              },
    478              function () {
    479                state.excludedTestError = "Session not found";
    480                renderExcludedTests();
    481              }
    482            );
    483          } else {
    484            state.excludedTestError = "Invalid session token";
    485            renderExcludedTests();
    486          }
    487        }
    488 
    489        function handleAddExludedTestsExcluded() {
    490          var excludedTestsTextArea = UI.getElement("excluded-tests-text");
    491          var token = excludedTestsTextArea.value;
    492          var tokenRegExp = new RegExp(
    493            "^[a-f0-9]{8}(-[a-f0-9]{0,4}|$)(-[a-f0-9]{0,4}|$)(-[a-f0-9]{0,4}|$)(-[a-f0-9]{0,12}|$)"
    494          );
    495          var configuration = state.configuration;
    496          var excludedTests = configuration.tests.exclude;
    497          if (tokenRegExp.test(token)) {
    498            WaveService.findToken(
    499              token,
    500              function (token) {
    501                WaveService.readSession(token, function (sessionConfig) {
    502                  var prevExcludedTests = sessionConfig.tests.exclude;
    503                  for (var test of prevExcludedTests) {
    504                    if (!test) continue;
    505                    if (excludedTests.indexOf(test) !== -1) continue;
    506                    excludedTests.push(test);
    507                  }
    508                  renderExcludedTests();
    509                });
    510              },
    511              function () {
    512                state.excludedTestError = "Session not found";
    513                renderExcludedTests();
    514              }
    515            );
    516          } else {
    517            state.excludedTestError = "Invalid session token";
    518            renderExcludedTests();
    519          }
    520        }
    521 
    522        function handleRemoveExcludedTest(test) {
    523          var configuration = state.configuration;
    524          var excludedTests = configuration.tests.exclude;
    525          var index = excludedTests.indexOf(test);
    526          excludedTests.splice(index, 1);
    527          renderExcludedTests();
    528        }
    529 
    530        function handleExcludeInputChange(type) {
    531          if (state.activeExcludeInput === type) {
    532            state.activeExcludeInput = null;
    533          } else {
    534            state.activeExcludeInput = type;
    535          }
    536          renderExcludedTests();
    537        }
    538 
    539        function showAddLabel() {
    540          state.addLabelVisible = true;
    541          renderLabelsField();
    542          UI.getElement("session-label-input").focus();
    543        }
    544 
    545        function hideAddLabel() {
    546          state.addLabelVisible = false;
    547          renderLabelsField();
    548        }
    549 
    550        function addLabel() {
    551          var label = UI.getElement("session-label-input").value;
    552          if (!label) return;
    553          state.configuration.labels.push(label);
    554          renderLabelsField();
    555          UI.getElement("session-label-input").focus();
    556        }
    557 
    558        function removeLabel(index) {
    559          const { configuration } = state;
    560          configuration.labels.splice(index, 1);
    561          renderLabelsField();
    562        }
    563 
    564        function resumeSession() {
    565          var resumeToken = UI.getElement("resume-token").value;
    566          if (!resumeToken) return;
    567 
    568          WaveService.resumeSession(
    569            state.configuration.token,
    570            resumeToken,
    571            function () {
    572              openResultsPage(resumeToken);
    573            }
    574          );
    575        }
    576 
    577        function render() {
    578          const configurationView = UI.createElement({
    579            element: "section",
    580            className: "section",
    581            children: [
    582              {
    583                className: "container",
    584                style: "margin-bottom: 2em",
    585                children: [
    586                  {
    587                    element: "img",
    588                    src: "res/wavelogo_2016.jpg",
    589                    className: "site-logo",
    590                  },
    591                  { className: "title", text: "Session Configuration" },
    592                ],
    593              },
    594              {
    595                id: "session-configuration",
    596              },
    597              {
    598                id: "resume-view",
    599                className: "container",
    600                style: "margin-bottom: 2em",
    601              },
    602            ],
    603          });
    604          const root = UI.getRoot();
    605          root.innerHTML = "";
    606          root.appendChild(configurationView);
    607          renderSessionConfiguration();
    608          renderResumeView();
    609        }
    610 
    611        function renderSessionConfiguration() {
    612          var configuration = state.configuration;
    613          var status = state.status;
    614          var sessionConfigurationView = UI.createElement({});
    615          var sessionConfiguration = UI.getElement("session-configuration");
    616          sessionConfiguration.innerHTML = "";
    617 
    618          if (state.expired) {
    619            var expiredIndicator = UI.createElement({
    620              className: "level container",
    621              style: "max-width: 500px",
    622              children: {
    623                element: "span",
    624                className: "level-item field column",
    625                children: [
    626                  {
    627                    element: "article",
    628                    className: "message is-danger",
    629                    id: "find-error",
    630                    children: [
    631                      {
    632                        text:
    633                          "Could not find any sessions! Try adding more characters of the token.",
    634                        className: "message-body",
    635                      },
    636                    ],
    637                    style: "display: none",
    638                  },
    639                  {
    640                    className: "label has-text-weight-normal",
    641                    text: "Session token:",
    642                  },
    643                  {
    644                    className: "field-body",
    645                    children: {
    646                      className: "field",
    647                      children: {
    648                        className: "control",
    649                        children: {
    650                          style: "display: flex; margin-bottom: 10px;",
    651                          children: [
    652                            {
    653                              element: "input",
    654                              inputType: "text",
    655                              className: "input is-family-monospace",
    656                              id: "token-fragment",
    657                              placeholder:
    658                                "First 8 characters or more of session token",
    659                              onKeyDown: function (event) {
    660                                if (event.key === "Enter") {
    661                                  handleConfigureSession();
    662                                }
    663                              },
    664                            },
    665                          ],
    666                        },
    667                      },
    668                    },
    669                  },
    670                  {
    671                    className: "field is-grouped is-grouped-right",
    672                    children: {
    673                      className: "control",
    674                      children: {
    675                        className: "button is-dark is-outlined",
    676                        children: [
    677                          {
    678                            element: "span",
    679                            className: "icon",
    680                            children: [
    681                              {
    682                                element: "i",
    683                                className: "fas fa-cog",
    684                              },
    685                            ],
    686                          },
    687                          { text: "Configure Session", element: "span" },
    688                        ],
    689                        onclick: function () {
    690                          handleConfigureSession();
    691                        },
    692                      },
    693                    },
    694                  },
    695                ],
    696              },
    697            });
    698            sessionConfigurationView.appendChild(expiredIndicator);
    699            sessionConfiguration.appendChild(sessionConfigurationView);
    700            return;
    701          }
    702 
    703          if (!configuration) {
    704            var loadingIndicator = createLoadingIndicator(
    705              "Loading configuration ..."
    706            );
    707            sessionConfigurationView.appendChild(loadingIndicator);
    708            sessionConfiguration.appendChild(sessionConfigurationView);
    709            return;
    710          }
    711 
    712          sessionConfiguration.parentNode.replaceChild(
    713            UI.createElement({
    714              id: "session-configuration",
    715              className: "container",
    716              style: "margin-bottom: 2em",
    717              children: [
    718                {
    719                  id: "token-field",
    720                },
    721                {
    722                  id: "expiration-field",
    723                },
    724                {
    725                  id: "labels-field",
    726                },
    727                {
    728                  id: "apis-field",
    729                },
    730                {
    731                  id: "exclude-field",
    732                },
    733                {
    734                  id: "types-field",
    735                },
    736                {
    737                  id: "references-field",
    738                },
    739                {
    740                  id: "buttons-field",
    741                },
    742              ],
    743            }),
    744            sessionConfiguration
    745          );
    746 
    747          renderTokenField();
    748          renderExpirationField();
    749          renderLabelsField();
    750          renderApisField();
    751          renderExcludeField();
    752          renderTypesField();
    753          renderReferencesField();
    754          renderButtonsField();
    755        }
    756 
    757        function renderTokenField() {
    758          var configuration = state.configuration;
    759          var tokenField = UI.getElement("token-field");
    760          tokenField.parentNode.replaceChild(
    761            UI.createElement({
    762              id: "token-field",
    763              className: "field is-horizontal",
    764              children: [
    765                {
    766                  className: "field-label",
    767                  children: { className: "label", text: "Token" },
    768                },
    769                {
    770                  className: "field-body",
    771                  children: {
    772                    className: "field",
    773                    children: {
    774                      className: "control",
    775                      text: configuration.token,
    776                    },
    777                  },
    778                },
    779              ],
    780            }),
    781            tokenField
    782          );
    783        }
    784 
    785        function renderExpirationField() {
    786          var status = state.status;
    787          var expirationField = UI.getElement("expiration-field");
    788          expirationField.parentNode.replaceChild(
    789            UI.createElement({
    790              id: "expiration-field",
    791              className: "field is-horizontal",
    792              children: [
    793                {
    794                  className: "field-label",
    795                  children: { className: "label", text: "Expires" },
    796                },
    797                {
    798                  className: "field-body",
    799                  children: {
    800                    className: "field",
    801                    children: {
    802                      className: "control",
    803                      text: status.expirationDate.toLocaleString(),
    804                    },
    805                  },
    806                },
    807              ],
    808            }),
    809            expirationField
    810          );
    811        }
    812 
    813        function renderLabelsField() {
    814          var addLabelVisible = state.addLabelVisible;
    815          var configuration = state.configuration;
    816          var labelsField = UI.getElement("labels-field");
    817 
    818          labelsField.parentNode.replaceChild(
    819            UI.createElement({
    820              id: "labels-field",
    821              className: "field is-horizontal",
    822              children: [
    823                {
    824                  className: "field-label",
    825                  children: { className: "label", text: "Labels" },
    826                },
    827                {
    828                  className: "field-body",
    829                  children: {
    830                    className: "field is-grouped is-grouped-multiline",
    831                    children: configuration.labels
    832                      .map((label, index) => ({
    833                        className: "control",
    834                        children: {
    835                          className: "tags has-addons",
    836                          children: [
    837                            {
    838                              element: "span",
    839                              className: "tag is-info",
    840                              text: label,
    841                            },
    842                            {
    843                              element: "a",
    844                              className: "tag is-delete",
    845                              onClick: () => removeLabel(index),
    846                            },
    847                          ],
    848                        },
    849                      }))
    850                      .concat(
    851                        addLabelVisible
    852                          ? [
    853                              {
    854                                className: "control field is-grouped",
    855                                children: [
    856                                  {
    857                                    element: "input",
    858                                    className: "input is-small control",
    859                                    style: "width: 10rem",
    860                                    id: "session-label-input",
    861                                    type: "text",
    862                                    onKeyUp: (event) =>
    863                                      event.keyCode === 13 ? addLabel() : null,
    864                                  },
    865                                  {
    866                                    className:
    867                                      "button is-dark is-outlined is-small is-rounded control",
    868                                    text: "save",
    869                                    onClick: addLabel,
    870                                  },
    871                                  {
    872                                    className:
    873                                      "button is-dark is-outlined is-small is-rounded control",
    874                                    text: "cancel",
    875                                    onClick: hideAddLabel,
    876                                  },
    877                                ],
    878                              },
    879                            ]
    880                          : [
    881                              {
    882                                className: "button is-rounded is-small",
    883                                text: "Add",
    884                                onClick: showAddLabel,
    885                              },
    886                            ]
    887                      ),
    888                  },
    889                },
    890              ],
    891            }),
    892            labelsField
    893          );
    894        }
    895 
    896        function renderApisField() {
    897          var apisField = UI.getElement("apis-field");
    898          apisField.parentNode.replaceChild(
    899            UI.createElement({
    900              id: "apis-field",
    901              className: "field is-horizontal",
    902              children: [
    903                {
    904                  className: "field-label",
    905                  children: [
    906                    { className: "label", text: "APIs" },
    907                    createSelectDeselectButtons(
    908                      function () {
    909                        selectAllTests();
    910                        renderApisField();
    911                      },
    912                      function () {
    913                        deselectAllTests();
    914                        renderApisField();
    915                      }
    916                    ),
    917                  ],
    918                },
    919                {
    920                  className: "field-body",
    921                  children: {
    922                    className: "field",
    923                    children: {
    924                      className: "control",
    925                      children: [
    926                        {
    927                          id: "api-error",
    928                        },
    929                        {
    930                          element: "ul",
    931                          className: "menu-list",
    932                          children: apis.map(function (api) {
    933                            return UI.createElement({
    934                              element: "li",
    935                              id: api.title,
    936                            });
    937                          }),
    938                        },
    939                      ],
    940                    },
    941                  },
    942                },
    943              ],
    944            }),
    945            apisField
    946          );
    947          renderApisList(apis);
    948          checkApiList();
    949        }
    950 
    951        function renderApisList(apis) {
    952          for (var api of apis) {
    953            renderApiList(api);
    954          }
    955        }
    956 
    957        function renderApiList(api) {
    958          var listItem = UI.getElement(api.title);
    959          var includedRatio = getIncludedRatio(api.path);
    960          var apiListItem = {
    961            element: "a",
    962            onClick: function (event) {
    963              if (!testFileSelectionEnabled) return;
    964              if (!state.expandedApis) state.expandedApis = {};
    965              state.expandedApis[api.path] = !state.expandedApis[api.path];
    966              renderApiList(api);
    967            },
    968            children: [
    969              {
    970                element: "input",
    971                type: "checkbox",
    972                style: "width: 1.3em; height: 1.3em;vertical-align: middle;",
    973                checked: includedRatio > 0,
    974                indeterminate: includedRatio > 0 && includedRatio < 1,
    975                onclick: function (event) {
    976                  event.stopPropagation();
    977                  if (includedRatio > 0) {
    978                    handleRemoveIncludedTest(api.path);
    979                  } else {
    980                    handleAddIncludedTest(api.path);
    981                  }
    982                  renderApiList(api);
    983                },
    984              },
    985              testFileSelectionEnabled
    986                ? {
    987                    element: "span",
    988                    style:
    989                      "display: inline-block;vertical-align: middle;margin-left:0.3em;width: 0.7em",
    990                    children: {
    991                      element: "i",
    992                      className:
    993                        state.expandedApis && state.expandedApis[api.path]
    994                          ? "fas fa-angle-down"
    995                          : "fas fa-angle-right",
    996                    },
    997                  }
    998                : null,
    999              {
   1000                style:
   1001                  "display: inline-block;vertical-align: middle;margin-left:0.3em;width: 90%",
   1002                text: api.title,
   1003              },
   1004            ],
   1005          };
   1006          listItem.innerHTML = "";
   1007          listItem.appendChild(UI.createElement(apiListItem));
   1008          if (state.expandedApis && state.expandedApis[api.path]) {
   1009            listItem.appendChild(createApiTestsList(api));
   1010          }
   1011          checkApiList();
   1012        }
   1013 
   1014        function createApiTestsList(api) {
   1015          if (!state.tests) {
   1016            WaveService.readTestList(
   1017              function (readTests) {
   1018                state.tests = readTests;
   1019                for (var api in state.tests) {
   1020                  if (hasIncludedTest("/" + api)) {
   1021                    handleRemoveIncludedTest("/" + api);
   1022                    handleAddIncludedTest("/" + api);
   1023                  }
   1024                }
   1025                renderApiList(this.api);
   1026              }.bind({ api: api })
   1027            );
   1028            return createLoadingIndicator("Loading tests ...");
   1029          } else {
   1030            var tests = state.tests[api.path.substr(1)];
   1031            var testListView = {
   1032              element: "ul",
   1033              children: [],
   1034            };
   1035 
   1036            testListView.children = testListView.children.concat(
   1037              tests
   1038                .sort()
   1039                .map(function (test) {
   1040                  return {
   1041                    element: "li",
   1042                    onclick: function (event) {
   1043                      handleIncludedTestToggle(test);
   1044                      renderApiList(api);
   1045                    },
   1046                    children: [
   1047                      {
   1048                        element: "a",
   1049                        children: [
   1050                          {
   1051                            element: "input",
   1052                            type: "checkbox",
   1053                            style:
   1054                              "width: 1.3em; height: 1.3em;vertical-align: middle;",
   1055                            checked: hasIncludedTest(test),
   1056                          },
   1057                          {
   1058                            style:
   1059                              "display: inline-block;vertical-align: middle;margin-left:0.3em;max-width: 90%",
   1060                            text: test,
   1061                          },
   1062                        ],
   1063                      },
   1064                    ],
   1065                  };
   1066                })
   1067            );
   1068            return UI.createElement(testListView);
   1069          }
   1070        }
   1071 
   1072        function renderExcludeField() {
   1073          var excludeField = UI.getElement("exclude-field");
   1074          excludeField.parentNode.replaceChild(
   1075            UI.createElement({
   1076              id: "exclude-field",
   1077              className: "field is-horizontal",
   1078              children: [
   1079                {
   1080                  className: "field-label",
   1081                  children: { className: "label", text: "Excluded Tests" },
   1082                },
   1083                {
   1084                  className: "field-body",
   1085                  children: {
   1086                    className: "field",
   1087                    children: {
   1088                      className: "control",
   1089                      children: { id: "excluded-tests-view" },
   1090                    },
   1091                  },
   1092                },
   1093              ],
   1094            }),
   1095            excludeField
   1096          );
   1097          renderExcludedTests();
   1098        }
   1099 
   1100        function renderTypesField() {
   1101          if (!testTypeSelectionEnabled) return;
   1102          var typesField = UI.getElement("types-field");
   1103          typesField.parentNode.replaceChild(
   1104            UI.createElement({
   1105              id: "types-field",
   1106              className: "field is-horizontal",
   1107              children: [
   1108                {
   1109                  className: "field-label",
   1110                  children: { className: "label", text: "Test Types" },
   1111                },
   1112                {
   1113                  className: "field-body",
   1114                  children: {
   1115                    className: "field",
   1116                    children: {
   1117                      className: "control",
   1118                      children: [
   1119                        isTestTypeListValid()
   1120                          ? null
   1121                          : createErrorMessage("Select at least one test type"),
   1122                      ].concat(createTestTypeList(types)),
   1123                    },
   1124                  },
   1125                },
   1126              ],
   1127            }),
   1128            typesField
   1129          );
   1130        }
   1131 
   1132        function renderReferencesField() {
   1133          if (referenceSessions.length === 0) {
   1134            return;
   1135          }
   1136          var referencesField = UI.getElement("references-field");
   1137          referencesField.parentNode.replaceChild(
   1138            UI.createElement({
   1139              id: "references-field",
   1140              className: "field is-horizontal",
   1141              children: [
   1142                {
   1143                  className: "field-label",
   1144                  children: [
   1145                    { className: "label", text: "Reference Browsers" },
   1146                    createSelectDeselectButtons(
   1147                      function () {
   1148                        selectAllRefSessions();
   1149                        renderReferencesField();
   1150                      },
   1151                      function () {
   1152                        deselectAllRefSessions();
   1153                        renderReferencesField();
   1154                      }
   1155                    ),
   1156                  ],
   1157                },
   1158                {
   1159                  className: "field-body",
   1160                  children: {
   1161                    className: "field",
   1162                    children: {
   1163                      className: "control",
   1164                      children: createRefSessionsList(referenceSessions),
   1165                    },
   1166                  },
   1167                },
   1168              ],
   1169            }),
   1170            referencesField
   1171          );
   1172        }
   1173 
   1174        function renderButtonsField() {
   1175          var buttonsField = UI.getElement("buttons-field");
   1176          buttonsField.parentNode.replaceChild(
   1177            UI.createElement({
   1178              id: "buttons-field",
   1179              className: "level level-right",
   1180              children: [
   1181                {
   1182                  element: "button",
   1183                  className: "button is-success",
   1184                  style: "margin-right: 0.3em",
   1185                  disabled: !isConfigurationValid(),
   1186                  onClick: function () {
   1187                    handleStart();
   1188                    renderButtonsField();
   1189                  },
   1190                  children: [
   1191                    {
   1192                      element: "span",
   1193                      className: "icon",
   1194                      children: [
   1195                        {
   1196                          element: "i",
   1197                          className: isSessionStarting()
   1198                            ? "fas fa-spinner fa-pulse"
   1199                            : "fas fa-play",
   1200                        },
   1201                      ],
   1202                    },
   1203                    {
   1204                      element: "span",
   1205                      text: isSessionStarting()
   1206                        ? "Starting Session ..."
   1207                        : "Start Session",
   1208                    },
   1209                  ],
   1210                },
   1211                {
   1212                  element: "button",
   1213                  className: "button",
   1214                  onClick: function () {
   1215                    handleDiscardChanges();
   1216                    renderSessionConfiguration();
   1217                  },
   1218                  disabled: isSessionStarting(),
   1219                  children: [
   1220                    {
   1221                      element: "span",
   1222                      className: "icon",
   1223                      children: [
   1224                        {
   1225                          element: "i",
   1226                          className: "fas fa-times",
   1227                        },
   1228                      ],
   1229                    },
   1230                    {
   1231                      element: "span",
   1232                      text: "Discard Changes",
   1233                    },
   1234                  ],
   1235                },
   1236              ],
   1237            }),
   1238            buttonsField
   1239          );
   1240        }
   1241 
   1242        function renderExcludedTests() {
   1243          var excludedTestsView = UI.getElement("excluded-tests-view");
   1244          if (!excludedTestsView) return;
   1245          excludedTestsView.innerHTML = "";
   1246 
   1247          var errorMessage = state.excludedTestError;
   1248          if (errorMessage) {
   1249            var error = createErrorMessage(errorMessage);
   1250            excludedTestsView.appendChild(UI.createElement(error));
   1251          }
   1252 
   1253          var excludeInputs = [
   1254            { title: "Add Raw", type: "raw" },
   1255            { title: "Add Malfunctioning", type: "malfunc" },
   1256            { title: "Add Previous Excluded", type: "excluded" },
   1257          ];
   1258 
   1259          var activeExcludeInput = state.activeExcludeInput;
   1260 
   1261          var excludedTestInputSwitch = UI.createElement({
   1262            className: "tabs is-centered is-toggle is-small",
   1263            children: {
   1264              element: "ul",
   1265              children: excludeInputs.map(function (input) {
   1266                return {
   1267                  element: "li",
   1268                  onClick: function () {
   1269                    handleExcludeInputChange(input.type);
   1270                  },
   1271                  className: (function () {
   1272                    if (activeExcludeInput === input.type) return "is-active";
   1273                    return "";
   1274                  })(),
   1275                  children: { element: "a", text: input.title },
   1276                };
   1277              }),
   1278            },
   1279          });
   1280          excludedTestsView.appendChild(excludedTestInputSwitch);
   1281 
   1282          if (activeExcludeInput === "raw") {
   1283            var rawInput = UI.createElement({
   1284              children: [
   1285                {
   1286                  className: "is-size-7",
   1287                  style: "margin-bottom: 20px",
   1288                  text:
   1289                    "Provide paths to test files or directories to exclude them from the session. One path per line, lines starting with # are omitted.",
   1290                },
   1291                {
   1292                  element: "textarea",
   1293                  className: "textarea",
   1294                  id: "excluded-tests-text",
   1295                },
   1296                {
   1297                  style: "margin-top: 10px",
   1298                  onClick: function () {
   1299                    handleAddExludedTestsRaw();
   1300                  },
   1301                  children: [
   1302                    {
   1303                      element: "button",
   1304                      className: "button",
   1305                      style: "margin-bottom: 20px",
   1306                      text: "Add",
   1307                    },
   1308                  ],
   1309                },
   1310              ],
   1311            });
   1312            excludedTestsView.appendChild(rawInput);
   1313          } else if (
   1314            activeExcludeInput === "malfunc" ||
   1315            activeExcludeInput === "excluded"
   1316          ) {
   1317            var malfuncInput = UI.createElement({
   1318              style: "margin-bottom: 1em",
   1319              children: [
   1320                {
   1321                  className: "is-size-7",
   1322                  style: "margin-bottom: 1em",
   1323                  text:
   1324                    activeExcludeInput === "malfunc"
   1325                      ? "Add malfunctioning tests from past sessions by providing at least the first eight characters of the session's token."
   1326                      : "Add excluded tests from past sessions by providing at least the first eight characters of the session's token.",
   1327                },
   1328                {
   1329                  className: "field is-horizontal",
   1330                  children: [
   1331                    {
   1332                      className: "field-label",
   1333                      children: { className: "label", text: "Session Token" },
   1334                    },
   1335                    {
   1336                      className: "field-body",
   1337                      children: {
   1338                        className: "field is-grouped is-multiline",
   1339                        children: [
   1340                          {
   1341                            id: "excluded-tests-text",
   1342                            className: "input",
   1343                            element: "input",
   1344                            type: "text",
   1345                          },
   1346                          {
   1347                            className: "button",
   1348                            style: "margin-left: 1em",
   1349                            text: "Add",
   1350                            onClick: function () {
   1351                              if (activeExcludeInput === "malfunc") {
   1352                                handleAddExludedTestsMalfunctioning();
   1353                              } else {
   1354                                handleAddExludedTestsExcluded();
   1355                              }
   1356                            },
   1357                          },
   1358                        ],
   1359                      },
   1360                    },
   1361                  ],
   1362                },
   1363              ],
   1364            });
   1365            excludedTestsView.appendChild(malfuncInput);
   1366          }
   1367 
   1368          var excludedTestsTable = createExcludedTestsTable();
   1369          var tableWrapper = UI.createElement({
   1370            style: "max-height: 250px; overflow: auto; margin-bottom: 10px",
   1371          });
   1372          tableWrapper.appendChild(excludedTestsTable);
   1373          excludedTestsView.appendChild(tableWrapper);
   1374        }
   1375 
   1376        function renderResumeView() {
   1377          var query = utils.parseQuery(location.search);
   1378          var resumeToken = query.resume;
   1379          if (!resumeToken) resumeToken = "";
   1380 
   1381          var renderResumeElement = UI.getElement("resume-view");
   1382          renderResumeElement.innerHTML = "";
   1383          if (state.expired) return;
   1384 
   1385          var heading = UI.createElement({
   1386            element: "h2",
   1387            className: "title is-5",
   1388            text: "Resume session",
   1389          });
   1390          renderResumeElement.appendChild(heading);
   1391 
   1392          var resumeControls = UI.createElement({
   1393            className: "columns",
   1394            children: [
   1395              {
   1396                className: "column",
   1397                children: {
   1398                  className: "field",
   1399                  children: [
   1400                    {
   1401                      element: "label",
   1402                      className: "label",
   1403                      text: "Token (first 8 characters or more)",
   1404                    },
   1405                    {
   1406                      className: "control",
   1407                      children: {
   1408                        element: "input",
   1409                        id: "resume-token",
   1410                        className: "input is-family-monospace tabbable",
   1411                        type: "text",
   1412                        style: "max-width: 30em",
   1413                        value: resumeToken,
   1414                      },
   1415                    },
   1416                  ],
   1417                },
   1418              },
   1419              {
   1420                className: "column",
   1421                style:
   1422                  "display: flex; align-items: flex-end; justify-content: flex-end",
   1423                children: {
   1424                  className: "button",
   1425                  onClick: function () {
   1426                    resumeSession();
   1427                  },
   1428                  children: [
   1429                    {
   1430                      element: "span",
   1431                      className: "icon",
   1432                      children: { element: "i", className: "fas fa-redo-alt" },
   1433                    },
   1434                    {
   1435                      element: "span",
   1436                      text: "Resume",
   1437                    },
   1438                  ],
   1439                },
   1440              },
   1441            ],
   1442          });
   1443          renderResumeElement.appendChild(resumeControls);
   1444        }
   1445 
   1446        function createExcludedTestsTable() {
   1447          var excludedTests = state.configuration.tests.exclude;
   1448          if (excludedTests.length === 0) {
   1449            return UI.createElement({
   1450              style: "text-align: center",
   1451              text: "- No Excluded Tests -",
   1452            });
   1453          }
   1454          var table = UI.createElement({
   1455            element: "table",
   1456            className: "table",
   1457            style: "width: 100%",
   1458            children: excludedTests.map(function (test) {
   1459              return {
   1460                element: "tr",
   1461                children: [
   1462                  { element: "td", style: "width: 100%;", text: test },
   1463                  {
   1464                    element: "td",
   1465                    children: {
   1466                      element: "button",
   1467                      className: "button is-small",
   1468                      onClick: function () {
   1469                        handleRemoveExcludedTest(test);
   1470                      },
   1471                      children: {
   1472                        element: "span",
   1473                        className: "icon",
   1474                        children: {
   1475                          element: "i",
   1476                          className: "fas fa-trash-alt",
   1477                        },
   1478                      },
   1479                    },
   1480                  },
   1481                ],
   1482              };
   1483            }),
   1484          });
   1485          return table;
   1486        }
   1487 
   1488        function createTestTypeList(types) {
   1489          return types.map((type) => ({
   1490            element: "button",
   1491            style: "margin-right: 0.3em; margin-bottom: 0.3em",
   1492            className: "button" + (hasTestType(type.value) ? " is-info" : ""),
   1493            text: type.title,
   1494            onClick: function (event) {
   1495              handleTestTypeToggle(type.value);
   1496              renderTypesField();
   1497            },
   1498          }));
   1499        }
   1500 
   1501        function createRefSessionsList(referenceSessions) {
   1502          return referenceSessions.map((session) => ({
   1503            element: "button",
   1504            className: "button" + (hasRefSession(session) ? " is-info" : ""),
   1505            style: "margin-right: 0.3em; margin-bottom: 0.3em; height: auto",
   1506            onClick: function () {
   1507              handleRefSessionToggle(session);
   1508              renderReferencesField();
   1509            },
   1510            children: [
   1511              {
   1512                element: "span",
   1513                className: "icon",
   1514                children: [{ element: "i", className: session.icon }],
   1515              },
   1516              {
   1517                element: "span",
   1518                children: [
   1519                  { text: session.title },
   1520                  {
   1521                    text: session.engine,
   1522                    style: "font-size: 0.8em",
   1523                  },
   1524                ],
   1525              },
   1526            ],
   1527          }));
   1528        }
   1529 
   1530        function createSelectDeselectButtons(onSelect, onDeselect) {
   1531          return {
   1532            style: "margin-top: 0.3em",
   1533            children: [
   1534              {
   1535                element: "button",
   1536                style: "margin-right: 0.3em",
   1537                className: "button is-rounded is-small",
   1538                text: "All",
   1539                onClick: onSelect,
   1540              },
   1541              {
   1542                element: "button",
   1543                className: "button is-rounded is-small",
   1544                text: "None",
   1545                onClick: onDeselect,
   1546              },
   1547            ],
   1548          };
   1549        }
   1550 
   1551        function createErrorMessage(message) {
   1552          return {
   1553            element: "article",
   1554            className: "message is-danger",
   1555            children: [
   1556              {
   1557                className: "message-body",
   1558                text: message,
   1559              },
   1560            ],
   1561          };
   1562        }
   1563 
   1564        function createLoadingIndicator(text) {
   1565          return UI.createElement({
   1566            className: "level",
   1567            children: {
   1568              element: "span",
   1569              className: "level-item",
   1570              children: [
   1571                {
   1572                  element: "i",
   1573                  className: "fas fa-spinner fa-pulse",
   1574                },
   1575                {
   1576                  style: "margin-left: 0.4em;",
   1577                  text: text,
   1578                },
   1579              ],
   1580            },
   1581          });
   1582        }
   1583      }
   1584    </script>
   1585  </body>
   1586 </html>