www/include/TestResultsHtmlTables.php
<?php

// Copyright 2020 Catchpoint Systems Inc.
// Use of this source code is governed by the Polyform Shield 1.0.0 license that can be
// found in the LICENSE.md file.

require_once INCLUDES_PATH . '/common_lib.inc';

class TestResultsHtmlTables
{
  /** @var TestInfo */
    private $testInfo;
  /** @var TestResults */
    private $testResults;
    private $testComplete;
    private $breakdown;
    private $hasVideo;
    private $hasScreenshots;
    private $firstViewMedianRun;
    private $tcpDumpViewSettings;
    private $isMultistep;

    private $waterfallDisplayed;
    private $screenshotDisplayed;

  /**
   * TestResultsHtmlTables constructor.
   * @param TestInfo $testInfo Test information
   * @param TestResults $testResults The results of the test
   * @param bool $testComplete True if the test is complete, false otherwise
   * @param string|null $median_metric The metric to use to determine the median. (load time by default)
   * @param string|null $tcpDumpViewSettings The settings for viewing a TCP dump (URL or null)
   */
    public function __construct($testInfo, $testResults, $testComplete, $median_metric, $tcpDumpViewSettings)
    {
        $this->testInfo = $testInfo;
        $this->testResults = $testResults;
        $this->testComplete = $testComplete;
        $this->breakdown = null;
        $this->hasVideo = $this->testInfo->hasVideo();
        $this->hasScreenshots = $this->testInfo->hasScreenshots();
        $this->firstViewMedianRun = $this->testResults->getMedianRunNumber($median_metric, false);
        $this->tcpDumpViewSettings = $tcpDumpViewSettings;
        $this->isMultistep = $this->testInfo->getSteps() > 1;
    }

    public function create()
    {
        $runs = $this->testInfo->getRuns();
        $this->waterfallDisplayed = false;
        $this->screenshotDisplayed = false;
        $out = "";
        for ($run = 1; $run <= $runs; $run++) {
            $runResults = $this->testResults->getRunResult($run, false);
            if ($runs > 1) {
                $out .=  '<h4><a name="run' . $run . '">Run ' . $run . ':</a></h4>';
            }
            if (!$runResults) {
                $error_str = $this->testComplete ? 'Test Error: Data is missing.' : 'Waiting for test result...';
                $out .=  '<p>' . htmlspecialchars($error_str) . '</p>';
            } else {
                $out .= $this->_createTableForRun($run);
            }
        }
        return $out;
    }

    private function _createTableForRun($run)
    {
        $fvMedian = $this->firstViewMedianRun;
        $out = "<div class=\"scrollableTable\"><table id=\"table$run\" class=\"pretty result\" align=\"center\" border=\"1\" cellpadding=\"20\" cellspacing=\"0\">\n";
        $columns = $this->_countTableColumns();
        $out .= '<thead>' . $this->_createTableHead() . '</thead>';

        $firstViewResults = $this->testResults->getRunResult($run, false);
        $hasRepeatView = !$this->testInfo->isFirstViewOnly() || $this->testResults->getRunResult($run, true);

        if ($this->isMultistep && $hasRepeatView) {
            $out .= $this->_createSeparationRow("First View", $columns);
        }
        $out .= $this->_createRunResultRows($run, false, $columns);

        if ($hasRepeatView) {
            if ($this->isMultistep) {
                $out .= $this->_createSeparationRow("Repeat View", $columns);
            }
            $out .= $this->_createRunResultRows($run, true, $columns);
        }

        if ($this->testComplete && $run == $fvMedian && $firstViewResults && $firstViewResults->isValid() && !$this->isMultistep) {
            $out .= $this->_createBreakdownRow($firstViewResults->getStepResult(1), $columns);
        }

        $out .= "</table>\n</div>\n";
        return $out;
    }

    private function _countTableColumns()
    {
        $columns = 2;
        if ($this->hasScreenshots) {
            $columns++;
        }
        if ($this->hasVideo) {
            $columns++;
        }
        return $columns;
    }

    private function _createSeparationRow($label, $colspan)
    {
        return "<tr><td colspan='$colspan' class='separation'>$label</td></tr>\n";
    }

  /**
   * @param int $run Run number
   * @param bool $cached False for first view, true for repeat view
   * @param int $tableColumns number of columns in the table
   * @return string The created markup
   */
    private function _createRunResultRows($run, $cached, $tableColumns)
    {
        $runResults = $this->testResults->getRunResult($run, $cached);
        if (!$runResults || !$runResults->isValid()) {
            return $this->_createErrorRow($run, $cached, $tableColumns);
        }

        $evenRow = false;
        $out = "";
        foreach ($runResults->getStepResults() as $stepResult) {
            $out .= $this->_createStepResultRow($stepResult, $evenRow);
            $evenRow = !$evenRow;
        }
        return $out;
    }

  /**
   * @param TestStepResult $stepResult
   * @param int $tableColumns Number of columsn in the table
   * @return string The created markup
   */
    private function _createBreakdownRow($stepResult, $tableColumns)
    {
        $urlGenerator = $stepResult->createUrlGenerator("", FRIENDLY_URLS);
        $b = getBreakdownForStep($stepResult->createTestPaths(), $urlGenerator, $requests);
        if (is_array($b)) {
            $this->breakdown[] = array('run' => $stepResult->getRunNumber(), 'data' => $b);
        }
        $out = "<tr>\n";

        $out .= "<td align=\"left\" valign=\"middle\">\n";
        $breakdownUrl = $urlGenerator->resultPage("breakdown");
        $out .= "<a href=\"$breakdownUrl\">Content Breakdown</a>";
        $out .= "</td>";

        $span = $tableColumns - 1;
        $out .= "<td align=\"left\" valign=\"middle\" colspan=\"$span\">";
        $run = $stepResult->getRunNumber();
        $out .= "<table><tr><td style=\"border:none;\"><div id=\"requests_$run\"></div></td>";
        $out .= "<td style=\"border:none;\"><div id=\"bytes_$run\"></div></td></tr></table>";
        $out .= "</td>\n";

        $out .= "</tr>\n";
        return $out;
    }

    public function getBreakdown()
    {
        return $this->breakdown;
    }

    private function _createTableHead()
    {
        $out =  "<tr>\n";
        $out .=  "<th align=\"center\" class=\"empty pin\" valign=\"middle\"></th>\n";
        $out .=  "<th align=\"center\" valign=\"middle\">Waterfall</th>\n";
        if ($this->hasScreenshots) {
            $out .=  '<th align="center" valign="middle">Screenshot</th>';
        }
        if ($this->hasVideo) {
            $out .=  '<th align="center" valign="middle">Video</th>';
        }
        $out .=  '</tr>';
        return $out;
    }

  /**
   * @param TestStepResult $stepResult
   * @param bool $evenRow
   * @return string Created markup
   */
    private function _createStepResultRow($stepResult, $evenRow)
    {
        $rowId = "run" . $stepResult->getRunNumber() . "_step" . $stepResult->getStepNumber();
        $out = "<tr id='$rowId' class='stepResultRow'>\n";
        $out .= $this->_createResultCell($stepResult, $evenRow);
        $out .= $this->_createWaterfallCell($stepResult, $evenRow);
        if ($this->hasScreenshots) {
            $out .= $this->_createScreenshotCell($stepResult, $evenRow);
        }
        if ($this->hasVideo) {
            $out .= $this->_createVideoCell($stepResult, $evenRow);
        }
        $out .= "</tr>\n";
        return $out;
    }

  /**
   * @param TestStepResult $stepResult
   * @param bool $even true for even rows
   * @return string The created markup
   */
    private function _createResultCell($stepResult, $even)
    {
        $evenClass = $even ? " even" : "";
        $out = "<td align=\"left\" class='resultCell$evenClass'>\n";
        if ($this->isMultistep) {
            $out .= FitText($stepResult->readableIdentifier(), 30);
        } else {
            $out .=  $stepResult->isCachedRun() ? "Repeat View" : "First View";
        }
        $out .=  $this->_getResultLabel($stepResult);
        $out .=  $this->_getCaptureLinks($stepResult);
        $out .=  $this->_getTimelineLinks($stepResult);
        $out .=  $this->_getTraceLinks($stepResult);
        $out .=  $this->_getNetlogLinks($stepResult);
        $out .=  $this->_getDebuglogLinks($stepResult);
        $out .=  '</td>';
        return $out;
    }

  /**
   * @param TestStepResult $stepResult
   * @return string The created markup
   */
    private function _createVideoCell($stepResult, $even)
    {
        $localPaths = $stepResult->createTestPaths();
        $nameOnlyPaths = $stepResult->createTestPaths("");
        $class = $even ? 'class="even"' : '';
        $out = "<td align=\"center\"  $class>";
        if (is_dir($localPaths->videoDir())) {
            $urlGenerator = $stepResult->createUrlGenerator("", false);
            $end = $this->getRequestEndParam($stepResult);

            $filmstripUrl = $urlGenerator->filmstripView($end);
            $out .=  "<a href=\"$filmstripUrl\">Filmstrip View</a><br>-<br>";

            $createUrl = $urlGenerator->createVideo($end);
            $out .=  "<a href=\"$createUrl\">Watch Video</a>";

            $rawVideoPath = $localPaths->rawDeviceVideo();
            if (is_file($rawVideoPath)) {
                $rawVideoUrl = htmlspecialchars($urlGenerator->getFile($nameOnlyPaths->rawDeviceVideo()));
                $out .=  "<br>-<br><a href=\"$rawVideoUrl\">Raw Device Video</a>";
            }
        } else {
            $out .=  "not available";
        }
        $out .=  '</td>';
        return $out;
    }

  /**
   * @param TestStepResult $stepResult
   * @param bool $even
   * @return string The created markup
   */
    private function _createWaterfallCell($stepResult, $even)
    {
        $end = $this->getRequestEndParam($stepResult);
        $endParam = $end ? "end=$end" : null;

        $urlGenerator = $stepResult->createUrlGenerator("", FRIENDLY_URLS && !$endParam);
        $hash = "#waterfall_view_step" . $stepResult->getStepNumber();
        $detailsUrl = $urlGenerator->resultPage("details", $endParam) . $hash;
        $thumbUrl = $urlGenerator->thumbnail("waterfall.png");
        $class = $even ? 'class="even"' : '';

        $out = "<td align=\"center\" >\n";
        $out .=  "<a href=\"$detailsUrl\"><img class=\"progress\" width=\"250\" src=\"$thumbUrl\"></a>\n";
        $out .=  "</td>\n";

        $this->waterfallDisplayed = true;
        return $out;
    }

  /**
   * @param TestStepResult $stepResult
   * @return string|null The $_REQUEST["end"] parameter if set and the run is the first view median run, null otherwise
   */
    private function getRequestEndParam($stepResult)
    {
        if (!$stepResult->isCachedRun() && $stepResult->getRunNumber() == $this->firstViewMedianRun && array_key_exists('end', $_REQUEST)) {
            return $_REQUEST['end'];
        }
        return null;
    }

  /**
   * @param TestStepResult $stepResult
   * @param bool $even
   * @return string The created markup
   */
    private function _createScreenshotCell($stepResult, $even)
    {
        $urlGenerator = $stepResult->createUrlGenerator("", FRIENDLY_URLS);
        $class = $even ? 'class="even"' : '';
        $screenShotUrl = $urlGenerator->resultPage("screen_shot") . "#step_" . $stepResult->getStepNumber();
        $thumbnailUrl = $urlGenerator->thumbnail("screen.jpg");
        $out = "<td align=\"center\"  $class>\n";
        $out .= "<a href=\"$screenShotUrl\"><img class=\"progress\" width=\"250\" src=\"$thumbnailUrl\"></a>\n";
        $out .= "</td>\n";
        return $out;
    }

  /**
   * @param TestStepResult $stepResult
   * @return string Markup with links
   */
    private function _getTimelineLinks($stepResult)
    {
        if (!$this->testInfo->hasTimeline()) {
            return "";
        }
        $localPaths = $stepResult->createTestPaths();
        if (
            !gz_is_file($localPaths->devtoolsTraceFile()) &&
            !gz_is_file($localPaths->devtoolsTimelineFile()) &&
            !gz_file($localPaths->devtoolsFile())
        ) {
            return "";
        }
        $urlGenerator = $stepResult->createUrlGenerator("", FRIENDLY_URLS);
        $downloadUrl = $urlGenerator->stepDetailPage("getTimeline");
        $viewUrl = $urlGenerator->stepDetailPage("chrome/timeline");
        $breakdownUrl = $urlGenerator->stepDetailPage("breakdownTimeline");

        $out = "<br><br>\n";
        $out .= "<a href=\"$downloadUrl\" title=\"Download Chrome Dev Tools Timeline\">Timeline</a>\n";
        $out .= " (<a href=\"$viewUrl\" target=\"_blank\" title=\"View Chrome Dev Tools Timeline\">view</a>)\n";
        $out .= "<br>\n";
        $out .= "<a href=\"$breakdownUrl\" title=\"View browser main-thread activity by event type\">Processing Breakdown</a>";
        return $out;
    }

  /**
   * @param TestStepResult $stepResult
   * @return string Markup with links
   */
    private function _getCaptureLinks($stepResult)
    {
        $localPaths = $stepResult->createTestPaths();
        if (!gz_is_file($localPaths->captureFile())) {
            return "";
        }
        $wpt_host = trim($_SERVER['HTTP_HOST']);
        $filenamePaths = $stepResult->createTestPaths("");
        $urlGenerator = $stepResult->createUrlGenerator("", false);

        $tcpdump_url = $urlGenerator->getGZip($filenamePaths->captureFile());
        $out = "<br><br>\n";
        $out .= "<a href=\"$tcpdump_url\" title=\"Download tcpdump session capture\">tcpdump</a>\n";
        if ($this->tcpDumpViewSettings) {
            $view_url = $this->tcpDumpViewSettings . urlencode("http://$wpt_host$tcpdump_url");
            $out .= " - (<a href=\"$view_url\" title=\"View tcpdump session capture\">view</a>)";
        }
        if (gz_is_file($localPaths->keylogFile())) {
            $keylogUrl = $urlGenerator->getGZip($filenamePaths->keylogFile());
            $out .= "<br>(<a href=\"$keylogUrl\" title=\"TLS key log file\">TLS Key Log</a>)";
        }
        return $out;
    }

  /**
   * @param TestStepResult $stepResult
   * @return string Markup with links
   */
    private function _getTraceLinks($stepResult)
    {
        $infoArray = $this->testInfo->getInfoArray();
        $localPaths = $stepResult->createTestPaths();
        if ((empty($infoArray["trace"]) && !$this->testInfo->hasTimeline()) || !gz_is_file($localPaths->devtoolsTraceFile())) {
            return "";
        }
        $filenamePaths = $stepResult->createTestPaths("");
        $urlGenerator = $stepResult->createUrlGenerator("", FRIENDLY_URLS);
        $zipUrl = $urlGenerator->getGZip($filenamePaths->devtoolsTraceFile());
        $viewUrl = $urlGenerator->stepDetailPage("chrome/trace");

        $out = "<br><br><a href=\"$zipUrl\" title=\"Download Chrome Trace\">Trace</a>\n";
        $out .= " (<a href=\"$viewUrl\" target=\"_blank\" title=\"View Chrome Trace\">view</a>)\n";
        return $out;
    }

  /**
   * @param TestStepResult $stepResult
   * @return string Markup with links
   */
    private function _getNetlogLinks($stepResult)
    {
        $localPaths = $stepResult->createTestPaths();
        if (!gz_is_file($localPaths->netlogFile())) {
            return "";
        }
        $urlGenerator = $stepResult->createUrlGenerator("", FRIENDLY_URLS);
        $zipUrl = $urlGenerator->getGZip($stepResult->createTestPaths("")->netlogFile());
        return "<br><br><a href=\"$zipUrl\" title=\"Download Network Log\">Net Log</a>";
    }

  /**
   * @param TestStepResult $stepResult
   * @return string Markup with links
   */
    private function _getDebuglogLinks($stepResult)
    {
        $localPaths = $stepResult->createTestPaths();
        if (!gz_is_file($localPaths->debugLogFile())) {
            return "";
        }
        $urlGenerator = $stepResult->createUrlGenerator("", FRIENDLY_URLS);
        $zipUrl = $urlGenerator->getGZip($stepResult->createTestPaths("")->debugLogFile());
        $ret = "<br><br><a href=\"$zipUrl\" title=\"Download Debug Log\">Debug Log</a>";
        if (gz_is_file($localPaths->devtoolsFile())) {
            $zipUrl = $urlGenerator->getGZip($stepResult->createTestPaths("")->devtoolsFile());
            $ret .= "<br><a href=\"$zipUrl\" title=\"Download Raw Devtools Events\">Devtools Events</a>";
        }
        return $ret;
    }

  /**
   * @param TestStepResult $stepResult
   * @return string Markup with label
   */
    private function _getResultLabel($stepResult)
    {
        $out = "";
        $error = $this->testInfo->getRunError($stepResult->getRunNumber(), $stepResult->isCachedRun());
        if ($error) {
            $out .= '<br>(Test Error: ' . htmlspecialchars($error) . ')';
        }
        $result = $stepResult->getMetric("result");
        $loadTime = $stepResult->getMetric("loadTime");
        if ($result !== null && $result !== 0 && $result !== 99999) {
            $out .= '<br>(Error: ' . LookupError($result) . ')';
        } elseif ($loadTime !== null) {
            $out .= '<br>(' . number_format($loadTime / 1000.0, 3) . 's)';
        }
        return $out;
    }

    private function _createErrorRow($run, $cached, $tableColumns)
    {
        $error = $this->testInfo->getRunError($run, $cached);
        $error_str = $error ? htmlspecialchars('Test Error: ' . $error) : "Test Data Missing";

        $runResults = $this->testResults->getRunResult($run, $cached);
        if ($runResults) {
            $stepResults = $runResults->getStepResults();
            if (isset($stepResults) && is_array($stepResults) && count($stepResults) > 0) {
                $stepResult = $stepResults[0];
                $localPaths = $stepResult->createTestPaths();
                if (gz_is_file($localPaths->debugLogFile())) {
                    $urlGenerator = $stepResult->createUrlGenerator("", FRIENDLY_URLS);
                    $zipUrl = $urlGenerator->getGZip($stepResult->createTestPaths("")->debugLogFile());
                    $error_str .= ". <a href=\"$zipUrl\">Debug Log</a>";
                }
            }
        }

        $cachedLabel = $cached ? "Repeat View" : "First View";
        $out = "<tr id='run${run}_step1' class='stepResultRow'>";
        $out .= "<td colspan=\"$tableColumns\" align=\"left\" valign=\"middle\">$cachedLabel: $error_str</td></tr>\n";
        return $out;
    }
}
