tor-browser

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

results.html (55867B)


      1 <!DOCTYPE html>
      2 <html lang="en" style="overflow: auto;">
      3  <head>
      4    <meta charset="UTF-8" />
      5    <meta name="viewport" content="width=device-width, initial-scale=1" />
      6    <title>Session Results - 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    <!-- <link rel="stylesheet" href="css/result.css" /> -->
     10    <script src="lib/utils.js"></script>
     11    <script src="lib/wave-service.js"></script>
     12    <script src="lib/ui.js"></script>
     13    <style>
     14      .site-logo {
     15        max-width: 300px;
     16        margin: 0 0 30px -15px;
     17      }
     18    </style>
     19  </head>
     20  <body>
     21    <script>
     22      let token = null;
     23      window.onload = () => {
     24        const query = utils.parseQuery(location.search);
     25        token = query.token;
     26        if (token) {
     27          resultUi.render();
     28          resultUi.refreshData();
     29        } else {
     30          location.href = WEB_ROOT + "overview.html" + location.search;
     31        }
     32        WaveService.addRecentSession(token);
     33      };
     34      const resultUi = {
     35        state: {
     36          details: null,
     37          results: null,
     38          referenceSessions: [],
     39          lastCompletedTests: [],
     40          malfunctioningTests: [],
     41          addLabelVisible: false,
     42        },
     43        refreshData: (toUpdate) => {
     44          WaveService.readStatus(function (config) {
     45            resultUi.state.reportsEnabled = config.reportsEnabled;
     46            resultUi.renderApiResults();
     47          });
     48          switch (toUpdate) {
     49            case "test_completed":
     50              resultUi.refreshSessionStatus(() => {
     51                resultUi.refreshSessionResults(() => {
     52                  resultUi.renderApiResults();
     53                });
     54              });
     55              resultUi.refreshLastCompletedTests(() => {
     56                resultUi.renderLastCompletedTests();
     57              });
     58              break;
     59            case "status":
     60              resultUi.refreshSessionStatus(() => {
     61                resultUi.renderControls();
     62                resultUi.renderSessionDetails();
     63              });
     64              break;
     65            case "":
     66            case null:
     67            case undefined:
     68              resultUi.refreshSessionConfiguration(() => {
     69                resultUi.refreshSessionStatus(() => {
     70                  resultUi.refreshSessionResults(() => {
     71                    resultUi.refreshReferenceSessions(() =>
     72                      resultUi.renderReferenceSessions()
     73                    );
     74                    resultUi.renderControls();
     75                    resultUi.renderSessionDetails();
     76                    resultUi.renderApiResults();
     77                    resultUi.renderExportView();
     78                    resultUi.refreshLastCompletedTests(() => {
     79                      resultUi.renderLastCompletedTests();
     80                    });
     81                    resultUi.refreshMalfunctioningTests(() => {
     82                      resultUi.renderMalfunctioningTests();
     83                    });
     84                  });
     85                });
     86              });
     87              break;
     88          }
     89        },
     90        refreshSessionConfiguration(callback = () => {}) {
     91          WaveService.readSession(token, (configuration) => {
     92            resultUi.state.configuration = configuration;
     93            callback(configuration);
     94          });
     95        },
     96        refreshSessionStatus(callback = () => {}) {
     97          WaveService.readSessionStatus(token, (status) => {
     98            resultUi.state.status = status;
     99            if (status.status !== "completed" && status.status !== "aborted")
    100              WaveService.addSessionEventListener(
    101                token,
    102                resultUi.handleSessionEvent
    103              );
    104            callback(status);
    105          });
    106        },
    107        refreshReferenceSessions(callback = () => {}) {
    108          const { configuration } = resultUi.state;
    109          if (!configuration) return;
    110          const { referenceTokens } = configuration;
    111          if (!referenceTokens) return;
    112          WaveService.readMultipleSessions(referenceTokens, (configuration) => {
    113            resultUi.state.referenceSessions = configuration;
    114            resultUi.renderReferenceSessions();
    115            callback(configuration);
    116          });
    117        },
    118        refreshSessionResults(callback = () => {}) {
    119          WaveService.readResultsCompact(token, (results) => {
    120            resultUi.state.results = results;
    121            callback(results);
    122          });
    123        },
    124        refreshLastCompletedTests(callback = () => {}) {
    125          if (resultUi.state.configuration.isPublic) return;
    126          WaveService.readLastCompletedTests(token, ["timeout"], (tests) => {
    127            resultUi.state.lastCompletedTests = tests;
    128            callback();
    129          });
    130        },
    131        refreshMalfunctioningTests(callback = () => {}) {
    132          WaveService.readMalfunctioningTests(token, (tests) => {
    133            resultUi.state.malfunctioningTests = tests;
    134            callback();
    135          });
    136        },
    137        handleSessionEvent(message) {
    138          resultUi.refreshData(message.type);
    139        },
    140        openResultsOverview() {
    141          location.href = WEB_ROOT + "overview.html";
    142        },
    143        stopSession() {
    144          WaveService.stopSession(token, resultUi.refreshData);
    145        },
    146        deleteSession() {
    147          WaveService.deleteSession(token, () =>
    148            resultUi.openResultsOverview()
    149          );
    150        },
    151        showDeleteModal() {
    152          const modal = UI.getElement("delete-modal");
    153          const className = modal.getAttribute("class");
    154          modal.setAttribute("class", className + " is-active");
    155        },
    156        hideDeleteModal() {
    157          const modal = UI.getElement("delete-modal");
    158          let className = modal.getAttribute("class");
    159          className = className.replace(" is-active", "");
    160          modal.setAttribute("class", className);
    161        },
    162        downloadApiResultJson(api) {
    163          const { results } = resultUi.state;
    164          WaveService.downloadApiResult(token, api);
    165        },
    166        openHtmlReport(api) {
    167          const { results } = resultUi.state;
    168          if (results[api].complete != results[api].total) return;
    169          WaveService.readReportUri(token, api, function (uri) {
    170            window.open(uri, "_blank");
    171          });
    172        },
    173        downloadFinishedApiJsons() {
    174          WaveService.downloadAllApiResults(token);
    175        },
    176        downloadHtmlZip() {
    177          WaveService.downloadResultsOverview(token);
    178        },
    179        downloadResults() {
    180          if (resultUi.state.status.status !== "completed") return;
    181          WaveService.downloadResults(token);
    182        },
    183        addMalfunctioningTest(testPath) {
    184          const { malfunctioningTests } = resultUi.state;
    185          if (malfunctioningTests.indexOf(testPath) !== -1) return;
    186          malfunctioningTests.push(testPath);
    187          WaveService.updateMalfunctioningTests(
    188            token,
    189            malfunctioningTests,
    190            () => {
    191              resultUi.renderMalfunctioningTests();
    192            }
    193          );
    194          resultUi.renderLastCompletedTests();
    195        },
    196        removeMalfunctioningTest(testPath) {
    197          const { malfunctioningTests } = resultUi.state;
    198          malfunctioningTests.splice(malfunctioningTests.indexOf(testPath), 1);
    199          WaveService.updateMalfunctioningTests(
    200            token,
    201            malfunctioningTests,
    202            () => {
    203              resultUi.renderMalfunctioningTests();
    204            }
    205          );
    206          resultUi.renderLastCompletedTests();
    207        },
    208        isTestOnMalfunctioningList(test) {
    209          const { malfunctioningTests } = resultUi.state;
    210          return malfunctioningTests.indexOf(test) !== -1;
    211        },
    212        showExcluded() {
    213          resultUi.state.showExcluded = true;
    214          resultUi.renderSessionDetails();
    215        },
    216        hideExcluded() {
    217          resultUi.state.showExcluded = false;
    218          resultUi.renderSessionDetails();
    219        },
    220        addLabel() {
    221          const label = UI.getElement("session-label-input").value;
    222          if (!label) return;
    223          const { configuration } = resultUi.state;
    224          configuration.labels.push(label);
    225          WaveService.updateLabels(token, configuration.labels);
    226          resultUi.renderSessionDetails();
    227          UI.getElement("session-label-input").focus();
    228        },
    229        removeLabel(index) {
    230          const { configuration } = resultUi.state;
    231          configuration.labels.splice(index, 1);
    232          WaveService.updateLabels(token, configuration.labels);
    233          resultUi.renderSessionDetails();
    234        },
    235        showAddLabel() {
    236          resultUi.state.addLabelVisible = true;
    237          resultUi.renderSessionDetails();
    238          UI.getElement("session-label-input").focus();
    239        },
    240        hideAddLabel() {
    241          resultUi.state.addLabelVisible = false;
    242          resultUi.renderSessionDetails();
    243        },
    244        render() {
    245          const resultView = UI.createElement({
    246            className: "section",
    247            children: [
    248              {
    249                className: "container",
    250                style: "margin-bottom: 2em",
    251                children: [
    252                  {
    253                    className: "columns",
    254                    children: [
    255                      {
    256                        className: "column",
    257                        children: [
    258                          {
    259                            element: "img",
    260                            src: "res/wavelogo_2016.jpg",
    261                            className: "site-logo",
    262                          },
    263                        ],
    264                      },
    265                      {
    266                        className: "column is-narrow",
    267                        children: {
    268                          className: "button is-dark is-outlined",
    269                          onclick: resultUi.openResultsOverview,
    270                          children: [
    271                            {
    272                              element: "span",
    273                              className: "icon",
    274                              children: [
    275                                {
    276                                  element: "i",
    277                                  className: "fas fa-arrow-left",
    278                                },
    279                              ],
    280                            },
    281                            {
    282                              text: "Results Overview",
    283                              element: "span",
    284                            },
    285                          ],
    286                        },
    287                      },
    288                    ],
    289                  },
    290                  {
    291                    className: "container",
    292                    children: {
    293                      className: "columns",
    294                      children: [
    295                        {
    296                          className: "column",
    297                          children: { className: "title", text: "Result" },
    298                        },
    299                        {
    300                          className: "column is-narrow",
    301                          children: { id: "controls" },
    302                        },
    303                      ],
    304                    },
    305                  },
    306                ],
    307              },
    308              {
    309                id: "session-details",
    310                className: "container",
    311                style: "margin-bottom: 2em",
    312              },
    313              {
    314                id: "last-completed-tests",
    315                className: "container",
    316                style: "margin-bottom: 2em",
    317              },
    318              {
    319                id: "api-results",
    320                className: "container",
    321                style: "margin-bottom: 2em",
    322              },
    323              {
    324                id: "timeout-files",
    325                className: "container",
    326                style: "margin-bottom: 2em",
    327              },
    328              {
    329                id: "export",
    330                className: "container",
    331                style: "margin-bottom: 2em",
    332              },
    333              {
    334                id: "malfunctioning-tests",
    335                className: "container",
    336                style: "margin-bottom: 2em",
    337              },
    338            ],
    339          });
    340          const root = UI.getRoot();
    341          root.innerHTML = "";
    342          root.appendChild(resultView);
    343          resultUi.renderControls();
    344          resultUi.renderSessionDetails();
    345          resultUi.renderApiResults();
    346          resultUi.renderExportView();
    347        },
    348        renderControls() {
    349          const { state } = resultUi;
    350          if (!state.status) return;
    351          const { status } = state.status;
    352          const { isPublic } = state.configuration;
    353          const controlsView = UI.createElement({
    354            className: "field is-grouped is-grouped-multiline",
    355          });
    356          if (
    357            status &&
    358            status !== "aborted" &&
    359            status !== "completed" &&
    360            status !== "pending"
    361          ) {
    362            const pauseResumeButton = UI.createElement({
    363              id: "pause-resume-button",
    364              className: "control button is-dark is-outlined",
    365              onclick: function () {
    366                if (status === "running") {
    367                  WaveService.pauseSession(token, resultUi.refreshData);
    368                } else {
    369                  WaveService.startSession(token, resultUi.refreshData);
    370                }
    371              },
    372              children: [
    373                {
    374                  element: "span",
    375                  className: "icon",
    376                  children: [
    377                    {
    378                      element: "i",
    379                      className:
    380                        status === "running" ? "fas fa-pause" : "fas fa-play",
    381                    },
    382                  ],
    383                },
    384                {
    385                  text: status === "running" ? "Pause" : "Resume",
    386                  element: "span",
    387                },
    388              ],
    389            });
    390            controlsView.appendChild(pauseResumeButton);
    391          }
    392 
    393          if (status && status !== "aborted" && status !== "completed") {
    394            const stopButton = UI.createElement({
    395              id: "stop-button",
    396              className: "control button is-dark is-outlined",
    397              onclick: resultUi.stopSession,
    398              children: [
    399                {
    400                  element: "span",
    401                  className: "icon",
    402                  children: [
    403                    {
    404                      element: "i",
    405                      className: "fas fa-square",
    406                    },
    407                  ],
    408                },
    409                {
    410                  text: "Stop",
    411                  element: "span",
    412                },
    413              ],
    414            });
    415            controlsView.appendChild(stopButton);
    416          }
    417          if (!isPublic) {
    418            const deleteButton = UI.createElement({
    419              id: "delete-button",
    420              className: "control button is-dark is-outlined",
    421              onclick: resultUi.showDeleteModal,
    422              children: [
    423                {
    424                  element: "span",
    425                  className: "icon",
    426                  children: [
    427                    {
    428                      element: "i",
    429                      className: "fas fa-trash-alt",
    430                    },
    431                  ],
    432                },
    433                {
    434                  text: "Delete",
    435                  element: "span",
    436                },
    437              ],
    438            });
    439            controlsView.appendChild(deleteButton);
    440          }
    441 
    442          const deleteModal = UI.createElement({
    443            id: "delete-modal",
    444            className: "modal",
    445            children: [
    446              {
    447                className: "modal-background",
    448                onclick: resultUi.hideDeleteModal,
    449              },
    450              {
    451                className: "modal-card",
    452                children: [
    453                  {
    454                    className: "modal-card-head",
    455                    children: [
    456                      {
    457                        element: "p",
    458                        className: "modal-card-title",
    459                        text: "Delete Session",
    460                      },
    461                    ],
    462                  },
    463                  {
    464                    className: "modal-card-body",
    465                    children: [
    466                      {
    467                        element: "p",
    468                        text: "Are you sure you want to delete this session?",
    469                      },
    470                      { element: "p", text: "This action cannot be undone." },
    471                    ],
    472                  },
    473                  {
    474                    className: "modal-card-foot",
    475                    children: [
    476                      {
    477                        className: "button is-danger",
    478                        text: "Delete Session",
    479                        onclick: resultUi.deleteSession,
    480                      },
    481                      {
    482                        className: "button",
    483                        text: "Cancel",
    484                        onclick: resultUi.hideDeleteModal,
    485                      },
    486                    ],
    487                  },
    488                ],
    489              },
    490            ],
    491          });
    492          controlsView.appendChild(deleteModal);
    493 
    494          const controls = UI.getElement("controls");
    495          controls.innerHTML = "";
    496          controls.appendChild(controlsView);
    497        },
    498        renderSessionDetails() {
    499          const { state } = resultUi;
    500          const { configuration, status, results } = state;
    501          if (!configuration || !status) return;
    502          const sessionDetailsView = UI.createElement({
    503            style: "margin-bottom: 20px",
    504          });
    505 
    506          const heading = UI.createElement({
    507            text: "Session details",
    508            className: "title is-4",
    509          });
    510          sessionDetailsView.appendChild(heading);
    511 
    512          const getTagStyle = (status) => {
    513            switch (status) {
    514              case "completed":
    515                return "is-success";
    516              case "running":
    517                return "is-info";
    518              case "aborted":
    519                return "is-danger";
    520              case "paused":
    521                return "is-warning";
    522              case "pending":
    523                return "is-primary";
    524            }
    525          };
    526          if (status.dateFinished) {
    527            if (state.durationInterval) clearInterval(state.durationInterval);
    528          } else if (status.dateStarted) {
    529            if (!state.durationInterval)
    530              state.durationInterval = setInterval(() => {
    531                UI.getElement("duration").innerHTML = utils.millisToTimeString(
    532                  Date.now() - status.dateStarted.getTime()
    533                );
    534              }, 1000);
    535          }
    536 
    537          const { addLabelVisible } = state;
    538          const { showExcluded } = state;
    539 
    540          const tokenField = UI.createElement({
    541            className: "field is-horizontal",
    542            children: [
    543              {
    544                className: "field-label",
    545                children: { className: "label", text: "Token" },
    546              },
    547              {
    548                className: "field-body",
    549                children: {
    550                  className: "field",
    551                  children: {
    552                    className: "control",
    553                    text: configuration.token,
    554                  },
    555                },
    556              },
    557            ],
    558          });
    559          sessionDetailsView.appendChild(tokenField);
    560 
    561          const userAgentField = UI.createElement({
    562            className: "field is-horizontal",
    563            children: [
    564              {
    565                className: "field-label",
    566                children: { className: "label", text: "User Agent" },
    567              },
    568              {
    569                className: "field-body",
    570                children: {
    571                  className: "field",
    572                  children: {
    573                    className: "control",
    574                    text: configuration.userAgent || "",
    575                  },
    576                },
    577              },
    578            ],
    579          });
    580          sessionDetailsView.appendChild(userAgentField);
    581 
    582          const testPathsField = UI.createElement({
    583            className: "field is-horizontal",
    584            children: [
    585              {
    586                className: "field-label",
    587                children: { className: "label", text: "Test Paths" },
    588              },
    589              {
    590                className: "field-body",
    591                children: {
    592                  className: "field",
    593                  children: {
    594                    className: "control",
    595                    text: configuration.tests.include
    596                      .reduce((text, test) => text + test + ", ", "")
    597                      .slice(0, -2),
    598                  },
    599                },
    600              },
    601            ],
    602          });
    603          sessionDetailsView.appendChild(testPathsField);
    604 
    605          const excludedTestsField = UI.createElement({
    606            className: "field is-horizontal",
    607            children: [
    608              {
    609                className: "field-label",
    610                children: { className: "label", text: "Excluded Test Paths" },
    611              },
    612              {
    613                className: "field-body",
    614                children: {
    615                  className: "field",
    616                  children: {
    617                    className: "control",
    618                    children: [
    619                      {
    620                        element: "span",
    621                        text: configuration.tests.exclude.length,
    622                      },
    623                      {
    624                        element: "span",
    625                        className: "button is-small is-rounded",
    626                        style: "margin-left: 10px",
    627                        text: showExcluded ? "hide" : "show",
    628                        onClick: showExcluded
    629                          ? resultUi.hideExcluded
    630                          : resultUi.showExcluded,
    631                      },
    632                      showExcluded
    633                        ? {
    634                            style:
    635                              "max-height: 250px; overflow: auto; margin-bottom: 10px",
    636                            children: configuration.tests.exclude.map(
    637                              (test) => ({
    638                                text: test,
    639                              })
    640                            ),
    641                          }
    642                        : null,
    643                    ],
    644                  },
    645                },
    646              },
    647            ],
    648          });
    649          sessionDetailsView.appendChild(excludedTestsField);
    650 
    651          const referenceSessionField = UI.createElement({
    652            style: "display: none",
    653            id: "reference-session-field",
    654            className: "field is-horizontal",
    655            children: [
    656              {
    657                className: "field-label",
    658                children: { className: "label", text: "Reference Sessions" },
    659              },
    660              {
    661                className: "field-body",
    662                children: {
    663                  className: "field",
    664                  children: {
    665                    className: "control",
    666                    children: { id: "reference-sessions" },
    667                  },
    668                },
    669              },
    670            ],
    671          });
    672          sessionDetailsView.appendChild(referenceSessionField);
    673 
    674          const totalTestFilesField = UI.createElement({
    675            className: "field is-horizontal",
    676            children: [
    677              {
    678                className: "field-label",
    679                children: { className: "label", text: "Total Test Files" },
    680              },
    681              {
    682                className: "field-body",
    683                children: {
    684                  className: "field",
    685                  children: {
    686                    className: "control",
    687                    text: Object.keys(results).reduce(
    688                      (sum, api) => (sum += results[api].total),
    689                      0
    690                    ),
    691                  },
    692                },
    693              },
    694            ],
    695          });
    696          sessionDetailsView.appendChild(totalTestFilesField);
    697 
    698          const statusField = UI.createElement({
    699            className: "field is-horizontal",
    700            children: [
    701              {
    702                className: "field-label",
    703                children: { className: "label", text: "Status" },
    704              },
    705              {
    706                className: "field-body",
    707                children: {
    708                  className: "field",
    709                  children: {
    710                    className: `control tag ${getTagStyle(status.status)}`,
    711                    text: status.status,
    712                  },
    713                },
    714              },
    715            ],
    716          });
    717          sessionDetailsView.appendChild(statusField);
    718 
    719          const timeoutsField = UI.createElement({
    720            className: "field is-horizontal",
    721            children: [
    722              {
    723                className: "field-label",
    724                children: { className: "label", text: "Test Timeouts" },
    725              },
    726              {
    727                className: "field-body",
    728                children: {
    729                  className: "field",
    730                  children: {
    731                    className: `control`,
    732                    text: Object.keys(configuration.timeouts).reduce(
    733                      (text, timeout) =>
    734                        `${text}${timeout}: ${
    735                          configuration.timeouts[timeout] / 1000
    736                        }s\n`,
    737                      ""
    738                    ),
    739                  },
    740                },
    741              },
    742            ],
    743          });
    744          sessionDetailsView.appendChild(timeoutsField);
    745 
    746          if (status.dateStarted) {
    747            const startedField = UI.createElement({
    748              className: "field is-horizontal",
    749              children: [
    750                {
    751                  className: "field-label",
    752                  children: { className: "label", text: "Date Started" },
    753                },
    754                {
    755                  className: "field-body",
    756                  children: {
    757                    className: "field",
    758                    children: {
    759                      className: `control`,
    760                      text: new Date(status.dateStarted).toLocaleString(),
    761                    },
    762                  },
    763                },
    764              ],
    765            });
    766            sessionDetailsView.appendChild(startedField);
    767          }
    768 
    769          if (status.dateFinished) {
    770            const finishedField = UI.createElement({
    771              className: "field is-horizontal",
    772              children: [
    773                {
    774                  className: "field-label",
    775                  children: { className: "label", text: "Date Finished" },
    776                },
    777                {
    778                  className: "field-body",
    779                  children: {
    780                    className: "field",
    781                    children: {
    782                      className: `control`,
    783                      text: new Date(status.dateFinished).toLocaleString(),
    784                    },
    785                  },
    786                },
    787              ],
    788            });
    789            sessionDetailsView.appendChild(finishedField);
    790          }
    791 
    792          if (status.dateStarted) {
    793            const durationField = UI.createElement({
    794              className: "field is-horizontal",
    795              children: [
    796                {
    797                  className: "field-label",
    798                  children: { className: "label", text: "Duration" },
    799                },
    800                {
    801                  className: "field-body",
    802                  children: {
    803                    className: "field",
    804                    children: {
    805                      className: `control`,
    806                      id: "duration",
    807                      text: utils.millisToTimeString(
    808                        status.dateFinished
    809                          ? status.dateFinished.getTime() -
    810                              status.dateStarted.getTime()
    811                          : Date.now() - status.dateStarted.getTime()
    812                      ),
    813                    },
    814                  },
    815                },
    816              ],
    817            });
    818            sessionDetailsView.appendChild(durationField);
    819          }
    820 
    821          const labelsField = UI.createElement({
    822            className: "field is-horizontal",
    823            children: [
    824              {
    825                className: "field-label",
    826                children: { className: "label", text: "Labels" },
    827              },
    828              {
    829                className: "field-body",
    830                children: {
    831                  className: "field is-grouped is-grouped-multiline",
    832                  children: configuration.labels
    833                    .map((label, index) => ({
    834                      className: "control",
    835                      children: {
    836                        className: "tags has-addons",
    837                        children: [
    838                          {
    839                            element: "span",
    840                            className: "tag is-info",
    841                            text: label,
    842                          },
    843                          {
    844                            element: "a",
    845                            className: "tag is-delete",
    846                            onClick: () => resultUi.removeLabel(index),
    847                          },
    848                        ],
    849                      },
    850                    }))
    851                    .concat(
    852                      resultUi.state.configuration.isPublic
    853                        ? []
    854                        : addLabelVisible
    855                        ? [
    856                            {
    857                              className: "control field is-grouped",
    858                              children: [
    859                                {
    860                                  element: "input",
    861                                  className: "input is-small control",
    862                                  style: "width: 10rem",
    863                                  id: "session-label-input",
    864                                  type: "text",
    865                                  onKeyUp: (event) =>
    866                                    event.keyCode === 13
    867                                      ? resultUi.addLabel()
    868                                      : null,
    869                                },
    870                                {
    871                                  className:
    872                                    "button is-dark is-outlined is-small is-rounded control",
    873                                  text: "save",
    874                                  onClick: resultUi.addLabel,
    875                                },
    876                                {
    877                                  className:
    878                                    "button is-dark is-outlined is-small is-rounded control",
    879                                  text: "cancel",
    880                                  onClick: resultUi.hideAddLabel,
    881                                },
    882                              ],
    883                            },
    884                          ]
    885                        : [
    886                            {
    887                              className: "button is-rounded is-small",
    888                              text: "Add",
    889                              onClick: resultUi.showAddLabel,
    890                            },
    891                          ]
    892                    ),
    893                },
    894              },
    895            ],
    896          });
    897          sessionDetailsView.appendChild(labelsField);
    898 
    899          const sessionDetails = UI.getElement("session-details");
    900          sessionDetails.innerHTML = "";
    901          sessionDetails.appendChild(sessionDetailsView);
    902          resultUi.renderReferenceSessions();
    903        },
    904        renderReferenceSessions() {
    905          const { referenceSessions } = resultUi.state;
    906          if (!referenceSessions || referenceSessions.length === 0) return;
    907          const referenceSessionsList = UI.createElement({
    908            className: "field is-grouped is-grouped-multiline",
    909          });
    910          const getBrowserIcon = (browser) => {
    911            switch (browser.toLowerCase()) {
    912              case "firefox":
    913                return "fab fa-firefox";
    914              case "edge":
    915                return "fab fa-edge";
    916              case "chrome":
    917              case "chromium":
    918                return "fab fa-chrome";
    919              case "safari":
    920              case "webkit":
    921                return "fab fa-safari";
    922            }
    923          };
    924          referenceSessions.forEach((session) => {
    925            const { token, browser } = session;
    926            const referenceSessionItem = UI.createElement({
    927              className:
    928                "control button is-dark is-small is-rounded is-outlined",
    929              onClick: () => WaveService.openSession(token),
    930              children: [
    931                {
    932                  element: "span",
    933                  className: "icon",
    934                  children: {
    935                    element: "i",
    936                    className: getBrowserIcon(browser.name),
    937                  },
    938                },
    939                {
    940                  element: "span",
    941                  text: token.split("-").shift(),
    942                },
    943              ],
    944            });
    945            referenceSessionsList.appendChild(referenceSessionItem);
    946          });
    947          const referenceSessionsTarget = UI.getElement("reference-sessions");
    948          referenceSessionsTarget.innerHTML = "";
    949          referenceSessionsTarget.appendChild(referenceSessionsList);
    950          const field = UI.getElement("reference-session-field");
    951          field.style["display"] = "flex";
    952        },
    953        renderLastCompletedTests() {
    954          if (resultUi.state.configuration.isPublic) return;
    955          const lastCompletedTestsView = UI.createElement({});
    956 
    957          const heading = UI.createElement({
    958            className: "title is-4",
    959            children: [
    960              { element: "span", text: "Last Timed-Out Test Files" },
    961              {
    962                element: "span",
    963                className: "title is-7",
    964                text: " (most recent first)",
    965              },
    966            ],
    967          });
    968          lastCompletedTestsView.appendChild(heading);
    969 
    970          const { lastCompletedTests } = resultUi.state;
    971          const testsTable = UI.createElement({
    972            element: "table",
    973            className: "table",
    974            style: "min-width: 100%",
    975            children: [
    976              {
    977                element: "thead",
    978                children: [
    979                  {
    980                    element: "tr",
    981                    children: [
    982                      { element: "td", text: "Test File" },
    983                      { element: "td", text: "Malfunctioning List" },
    984                    ],
    985                  },
    986                ],
    987              },
    988              {
    989                element: "tbody",
    990                children: lastCompletedTests.map(({ path, status }) => ({
    991                  element: "tr",
    992                  children: [
    993                    { element: "td", text: path },
    994                    {
    995                      element: "td",
    996                      children: [
    997                        {
    998                          element: "button",
    999                          className: "button is-dark is-outlined is-small",
   1000                          onClick: () => resultUi.addMalfunctioningTest(path),
   1001                          title: "Add to malfunctioning tests list.",
   1002                          children: [
   1003                            {
   1004                              element: "span",
   1005                              className: "icon",
   1006                              children: [
   1007                                {
   1008                                  element: "i",
   1009                                  className: resultUi.isTestOnMalfunctioningList(
   1010                                    path
   1011                                  )
   1012                                    ? "fas fa-check"
   1013                                    : "fas fa-plus",
   1014                                },
   1015                              ],
   1016                            },
   1017                          ],
   1018                        },
   1019                      ],
   1020                    },
   1021                  ],
   1022                })),
   1023              },
   1024            ],
   1025          });
   1026          if (lastCompletedTests.length > 0) {
   1027            lastCompletedTestsView.appendChild(
   1028              UI.createElement({
   1029                className: "container",
   1030                style: "overflow-x: auto;",
   1031                id: "last-completed-overflow",
   1032                children: testsTable,
   1033              })
   1034            );
   1035          } else {
   1036            const noTestsLabel = UI.createElement({
   1037              text: "- No Timed-Out Tests -",
   1038              style: "text-align: center",
   1039            });
   1040            lastCompletedTestsView.appendChild(noTestsLabel);
   1041          }
   1042 
   1043          UI.saveScrollPosition("last-completed-overflow");
   1044 
   1045          const lastCompletedTestsElement = UI.getElement(
   1046            "last-completed-tests"
   1047          );
   1048          lastCompletedTestsElement.innerHTML = "";
   1049          lastCompletedTestsElement.appendChild(lastCompletedTestsView);
   1050 
   1051          UI.loadScrollPosition("last-completed-overflow");
   1052        },
   1053        renderApiResults() {
   1054          const { results, status } = resultUi.state;
   1055 
   1056          const apiResultsView = UI.createElement({
   1057            style: "margin-bottom: 20px",
   1058          });
   1059 
   1060          const heading = UI.createElement({
   1061            text: "API Results",
   1062            className: "title is-4",
   1063          });
   1064          apiResultsView.appendChild(heading);
   1065 
   1066          if (!results) {
   1067            const loadingIndicator = UI.createElement({
   1068              className: "level",
   1069              children: {
   1070                element: "span",
   1071                className: "level-item",
   1072                children: [
   1073                  {
   1074                    element: "i",
   1075                    className: "fas fa-spinner fa-pulse",
   1076                  },
   1077                  {
   1078                    style: "margin-left: 0.4em;",
   1079                    text: "Loading results ...",
   1080                  },
   1081                ],
   1082              },
   1083            });
   1084            apiResultsView.appendChild(loadingIndicator);
   1085 
   1086            const apiResults = UI.getElement("api-results");
   1087            apiResults.innerHTML = "";
   1088            apiResults.appendChild(apiResultsView);
   1089            return;
   1090          }
   1091 
   1092          const width = status.status === "running" ? "7.5em" : "auto";
   1093          const header = UI.createElement({
   1094            element: "thead",
   1095            children: [
   1096              {
   1097                element: "tr",
   1098                children: [
   1099                  { element: "th", text: "API" },
   1100                  { element: "th", text: "Pass", style: `min-width: ${width}` },
   1101                  { element: "th", text: "Fail", style: `min-width: ${width}` },
   1102                  {
   1103                    element: "th",
   1104                    text: "Timeout",
   1105                    style: `min-width: ${width}`,
   1106                  },
   1107                  {
   1108                    element: "th",
   1109                    text: "Not Run",
   1110                    style: `min-width: ${width}`,
   1111                  },
   1112                  {
   1113                    element: "th",
   1114                    text: "Test Files Run",
   1115                    style: `min-width: ${width}`,
   1116                  },
   1117                  { element: "th", text: "Export" },
   1118                ],
   1119              },
   1120            ],
   1121          });
   1122 
   1123          const apis = Object.keys(results).sort((apiA, apiB) =>
   1124            apiA.toLowerCase() > apiB.toLowerCase() ? 1 : -1
   1125          );
   1126 
   1127          const rows = apis.map((api) => {
   1128            const {
   1129              complete = 0,
   1130              pass = 0,
   1131              fail = 0,
   1132              timeout = 0,
   1133              timeoutfiles = [],
   1134              not_run: notRun = 0,
   1135              total,
   1136            } = results[api];
   1137            isDone = results[api].complete == results[api].total;
   1138            const totalTestResults = pass + fail + timeout + notRun;
   1139            return UI.createElement({
   1140              element: "tr",
   1141              style: "white-space: nowrap",
   1142              children: [
   1143                { element: "td", text: api },
   1144                {
   1145                  element: "td",
   1146                  children: {
   1147                    style: `color: hsl(141, 71%, 38%); overflow: visible; white-space: nowrap; width: ${width}`,
   1148                    text: `${pass} (${utils.percent(pass, totalTestResults)}%)`,
   1149                  },
   1150                },
   1151                {
   1152                  element: "td",
   1153                  children: {
   1154                    className: "has-text-danger",
   1155                    style: `overflow: visible; white-space: nowrap; width: ${width}`,
   1156                    text: `${fail} (${utils.percent(fail, totalTestResults)}%)`,
   1157                  },
   1158                },
   1159                {
   1160                  element: "td",
   1161                  children: {
   1162                    style: `color: hsl(48, 100%, 40%); overflow: visible; white-space: nowrap; width: ${width}`,
   1163                    text: `${timeout} (${utils.percent(
   1164                      timeout,
   1165                      totalTestResults
   1166                    )}%)`,
   1167                  },
   1168                },
   1169                {
   1170                  element: "td",
   1171                  children: {
   1172                    className: "has-text-info",
   1173                    style: `overflow: visible; white-space: nowrap; width: ${width}`,
   1174                    text: `${notRun} (${utils.percent(
   1175                      notRun,
   1176                      totalTestResults
   1177                    )}%)`,
   1178                  },
   1179                },
   1180                {
   1181                  element: "td",
   1182                  children: {
   1183                    style: `overflow: visible; white-space: nowrap; width: ${width}`,
   1184                    text: `${complete}/${total} (${utils.percent(
   1185                      complete,
   1186                      total
   1187                    )}%)`,
   1188                  },
   1189                },
   1190                {
   1191                  element: "td",
   1192                  children: {
   1193                    className: "field has-addons",
   1194                    children: [
   1195                      {
   1196                        className: "control",
   1197                        children: {
   1198                          className: "button is-dark is-outlined is-small",
   1199                          onclick: () => resultUi.downloadApiResultJson(api),
   1200                          text: "json",
   1201                          title: `Download results of ${api} API as JSON file.`,
   1202                        },
   1203                      },
   1204                      resultUi.state.reportsEnabled
   1205                        ? {
   1206                            className: "control",
   1207                            children: {
   1208                              className: "button is-dark is-outlined is-small",
   1209                              disabled: !isDone,
   1210                              onclick: () => resultUi.openHtmlReport(api),
   1211                              text: "report",
   1212                              title: `Show results of ${api} API in WPT Report format.`,
   1213                            },
   1214                          }
   1215                        : null,
   1216                    ],
   1217                  },
   1218                },
   1219              ],
   1220            });
   1221          });
   1222 
   1223          const { pass, fail, timeout, not_run, complete, total } = apis.reduce(
   1224            (sum, api) => {
   1225              Object.keys(sum).forEach(
   1226                (key) => (sum[key] += results[api][key] ? results[api][key] : 0)
   1227              );
   1228              return sum;
   1229            },
   1230            { complete: 0, total: 0, pass: 0, fail: 0, timeout: 0, not_run: 0 }
   1231          );
   1232          const totalTestResults = pass + fail + timeout + not_run;
   1233 
   1234          const footer = UI.createElement({
   1235            element: "tfoot",
   1236            children: [
   1237              {
   1238                element: "tr",
   1239                children: [
   1240                  { element: "th", text: "Total" },
   1241                  {
   1242                    element: "th",
   1243                    children: {
   1244                      style: `color: hsl(141, 71%, 38%); overflow: visible; white-space: nowrap; width: ${width}`,
   1245                      text: `${pass} (${utils.percent(
   1246                        pass,
   1247                        totalTestResults
   1248                      )}%)`,
   1249                    },
   1250                  },
   1251                  {
   1252                    element: "th",
   1253                    children: {
   1254                      style: `overflow: visible; white-space: nowrap; width: ${width}`,
   1255                      className: "has-text-danger",
   1256                      text: `${fail} (${utils.percent(
   1257                        fail,
   1258                        totalTestResults
   1259                      )}%)`,
   1260                    },
   1261                  },
   1262                  {
   1263                    element: "th",
   1264                    children: {
   1265                      style: `color: hsl(48, 100%, 40%); overflow: visible; white-space: nowrap; width: ${width}`,
   1266                      text: `${timeout} (${utils.percent(
   1267                        timeout,
   1268                        totalTestResults
   1269                      )}%)`,
   1270                    },
   1271                  },
   1272                  {
   1273                    element: "th",
   1274                    children: {
   1275                      style: `overflow: visible; white-space: nowrap; width: ${width}`,
   1276                      className: "has-text-info",
   1277                      text: `${not_run} (${utils.percent(
   1278                        not_run,
   1279                        totalTestResults
   1280                      )}%)`,
   1281                    },
   1282                  },
   1283                  {
   1284                    element: "th",
   1285                    children: {
   1286                      style: `overflow: visible; white-space: nowrap; width: ${width}`,
   1287                      text: `${complete}/${total} (${utils.percent(
   1288                        complete,
   1289                        total
   1290                      )}%)`,
   1291                    },
   1292                  },
   1293                  { element: "th" },
   1294                ],
   1295              },
   1296            ],
   1297          });
   1298 
   1299          const resultsTable = UI.createElement({
   1300            className: "container",
   1301            style: "overflow-x: auto",
   1302            id: "results-overflow",
   1303            children: {
   1304              element: "table",
   1305              className: "table",
   1306              id: "results-table",
   1307              style:
   1308                "width: 100%; min-width: 30em; border-radius: 3px; border: 2px solid hsl(0, 0%, 86%);",
   1309              children: [header, { element: "tbody", children: rows }, footer],
   1310            },
   1311          });
   1312          apiResultsView.appendChild(resultsTable);
   1313 
   1314          UI.saveScrollPosition("results-overflow");
   1315 
   1316          const apiResults = UI.getElement("api-results");
   1317          apiResults.innerHTML = "";
   1318          apiResults.appendChild(apiResultsView);
   1319 
   1320          UI.loadScrollPosition("results-overflow");
   1321        },
   1322        renderExportView() {
   1323          const { status } = resultUi.state;
   1324          if (!status) return;
   1325 
   1326          const exportElement = UI.getElement("export");
   1327          exportElement.innerHTML = "";
   1328 
   1329          const heading = UI.createElement({
   1330            className: "title is-4",
   1331            text: "Export",
   1332          });
   1333          exportElement.appendChild(heading);
   1334 
   1335          const resultsField = UI.createElement({
   1336            className: "field is-horizontal",
   1337            children: [
   1338              {
   1339                className: "field-label",
   1340                children: { className: "label", text: "Results" },
   1341              },
   1342              {
   1343                className: "field-body",
   1344                children: {
   1345                  className: "control columns",
   1346                  style: "width: 100%",
   1347                  children: [
   1348                    {
   1349                      className: "column is-9",
   1350                      text:
   1351                        "Download results for import into other WMAS Test Suite instances.",
   1352                    },
   1353                    {
   1354                      className: "column is-3",
   1355                      children: {
   1356                        className:
   1357                          "button is-dark is-outlined is-small is-fullwidth",
   1358                        onClick: resultUi.downloadResults,
   1359                        disabled: status.status !== "completed",
   1360                        children: [
   1361                          {
   1362                            element: "span",
   1363                            className: "icon",
   1364                            children: {
   1365                              element: "i",
   1366                              className: "fas fa-file-archive",
   1367                            },
   1368                          },
   1369                          { element: "span", text: "Download Zip" },
   1370                        ],
   1371                      },
   1372                    },
   1373                  ],
   1374                },
   1375              },
   1376            ],
   1377          });
   1378          exportElement.appendChild(resultsField);
   1379 
   1380          const jsonField = UI.createElement({
   1381            className: "field is-horizontal",
   1382            children: [
   1383              {
   1384                className: "field-label",
   1385                children: {
   1386                  className: "label",
   1387                  text: "All JSON Files",
   1388                },
   1389              },
   1390              {
   1391                className: "field-body",
   1392                children: {
   1393                  className: "control columns",
   1394                  style: "width: 100%",
   1395                  children: [
   1396                    {
   1397                      className: "column is-9",
   1398                      text:
   1399                        "Download JSON files containing results of completed test files.",
   1400                    },
   1401                    {
   1402                      className: "column is-3",
   1403                      children: {
   1404                        className:
   1405                          "button is-dark is-outlined is-small is-fullwidth",
   1406                        onclick: resultUi.downloadFinishedApiJsons,
   1407                        children: [
   1408                          {
   1409                            element: "span",
   1410                            className: "icon",
   1411                            children: {
   1412                              element: "i",
   1413                              className: "fas fa-file-archive",
   1414                            },
   1415                          },
   1416                          { element: "span", text: "Download Zip" },
   1417                        ],
   1418                      },
   1419                    },
   1420                  ],
   1421                },
   1422              },
   1423            ],
   1424          });
   1425          exportElement.appendChild(jsonField);
   1426 
   1427          const htmlField = UI.createElement({
   1428            className: "field is-horizontal",
   1429            children: [
   1430              {
   1431                className: "field-label",
   1432                children: {
   1433                  className: "label",
   1434                  text: "Session result HTML",
   1435                },
   1436              },
   1437              {
   1438                className: "field-body",
   1439                children: {
   1440                  className: "control columns",
   1441                  style: "width: 100%",
   1442                  children: [
   1443                    {
   1444                      className: "column is-9",
   1445                      text:
   1446                        "Download this sessions result as standalone HTML page, similar to this page.",
   1447                    },
   1448                    {
   1449                      className: "column is-3",
   1450                      children: {
   1451                        className:
   1452                          "button is-dark is-outlined is-small is-fullwidth",
   1453                        onClick: resultUi.downloadHtmlZip,
   1454                        children: [
   1455                          {
   1456                            element: "span",
   1457                            className: "icon",
   1458                            children: {
   1459                              element: "i",
   1460                              className: "fas fa-code",
   1461                            },
   1462                          },
   1463                          { element: "span", text: "Download HTML" },
   1464                        ],
   1465                      },
   1466                    },
   1467                  ],
   1468                },
   1469              },
   1470            ],
   1471          });
   1472          exportElement.appendChild(htmlField);
   1473        },
   1474        renderMalfunctioningTests() {
   1475          const malfunctioningTestsView = UI.createElement({});
   1476          const heading = UI.createElement({
   1477            className: "title is-4",
   1478            text: "Malfunctioning Tests",
   1479          });
   1480          malfunctioningTestsView.appendChild(heading);
   1481 
   1482          const { malfunctioningTests } = resultUi.state;
   1483          const testsTable = UI.createElement({
   1484            element: "table",
   1485            className: "table",
   1486            style: "min-width: 100%",
   1487            children: [
   1488              {
   1489                element: "thead",
   1490                children: [
   1491                  {
   1492                    element: "tr",
   1493                    children: [
   1494                      { element: "td", text: "Test File" },
   1495                      { element: "td", text: "" },
   1496                    ],
   1497                  },
   1498                ],
   1499              },
   1500              {
   1501                element: "tbody",
   1502                children: malfunctioningTests.map((path) => ({
   1503                  element: "tr",
   1504                  children: [
   1505                    { element: "td", text: path },
   1506                    {
   1507                      element: "td",
   1508                      children: resultUi.state.configuration.isPublic
   1509                        ? null
   1510                        : {
   1511                            element: "button",
   1512                            className: "button is-dark is-outlined is-small",
   1513                            onClick: () =>
   1514                              resultUi.removeMalfunctioningTest(path),
   1515                            title: "Remove from malfunctioning tests list.",
   1516                            children: [
   1517                              {
   1518                                element: "span",
   1519                                className: "icon",
   1520                                children: [
   1521                                  {
   1522                                    element: "i",
   1523                                    className: "fas fa-trash-alt",
   1524                                  },
   1525                                ],
   1526                              },
   1527                            ],
   1528                          },
   1529                    },
   1530                  ],
   1531                })),
   1532              },
   1533            ],
   1534          });
   1535          if (malfunctioningTests.length > 0) {
   1536            malfunctioningTestsView.appendChild(
   1537              UI.createElement({
   1538                className: "container",
   1539                style: "overflow-x: auto",
   1540                id: "malfunctioning-overflow",
   1541                children: testsTable,
   1542              })
   1543            );
   1544          } else {
   1545            const noTestsLabel = UI.createElement({
   1546              text: "- No Tests Available -",
   1547              style: "text-align: center",
   1548            });
   1549            malfunctioningTestsView.appendChild(noTestsLabel);
   1550          }
   1551 
   1552          UI.saveScrollPosition("malfunctioning-overflow");
   1553 
   1554          const malfunctioningTestsElement = UI.getElement(
   1555            "malfunctioning-tests"
   1556          );
   1557          malfunctioningTestsElement.innerHTML = "";
   1558          malfunctioningTestsElement.appendChild(malfunctioningTestsView);
   1559 
   1560          UI.loadScrollPosition("malfunctioning-overflow");
   1561        },
   1562      };
   1563    </script>
   1564  </body>
   1565 </html>