Changeset 234993 in webkit


Ignore:
Timestamp:
Aug 17, 2018, 11:56:45 AM (7 years ago)
Author:
Simon Fraser
Message:

Modernize results.html
https://bugs.webkit.org/show_bug.cgi?id=188690

Reviewed by Alexey Proskuryakov.

results.html, which is used to show layout test results, had some very old-school
HTML string building to create the tables of test results, making it hard to hack on.

Modernize it, using ES6 classes for the major actors, and using DOM API to build most
of the content.

The page is functionally the same (other than the addition of a missing 'History" column header).

  • fast/harness/results-expected.txt:
  • fast/harness/results.html:
Location:
trunk/LayoutTests
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/LayoutTests/ChangeLog

    r234974 r234993  
     12018-08-17  Simon Fraser  <simon.fraser@apple.com>
     2
     3        Modernize results.html
     4        https://bugs.webkit.org/show_bug.cgi?id=188690
     5
     6        Reviewed by Alexey Proskuryakov.
     7       
     8        results.html, which is used to show layout test results, had some very old-school
     9        HTML string building to create the tables of test results, making it hard to hack on.
     10       
     11        Modernize it, using ES6 classes for the major actors, and using DOM API to build most
     12        of the content.
     13       
     14        The page is functionally the same (other than the addition of a missing 'History" column header).
     15
     16        * fast/harness/results-expected.txt:
     17        * fast/harness/results.html:
     18
    1192018-08-16  Devin Rousso  <drousso@apple.com>
    220
  • trunk/LayoutTests/fast/harness/results-expected.txt

    r234928 r234993  
    66
    77+http/tests/contentextensions/top-url.html      crash log sample        history
    8 Other Crashes (2): flag all
     8Other crashes (2): flag all
    99
    1010+DumpRenderTree-54888   crash log
     
    3232Tests expected to fail but passed (4): flag all
    3333
    34 test    expected failure
     34test    expected failure        history
    3535canvas/philip/tests/2d.gradient.interpolate.solid.html  fail    history
    3636editing/spelling/spelling-marker-includes-hyphen.html   image   history
  • trunk/LayoutTests/fast/harness/results.html

    r217470 r234993  
    2121}
    2222
     23a.clickable {
     24    color: blue;
     25    cursor: pointer;
     26    margin-left: 0.2em;
     27}
     28
    2329tr:not(.results-row) td {
    2430    white-space: nowrap;
     
    3339}
    3440
    35 td {
     41th, td {
    3642    padding: 1px 4px;
    3743}
     
    7682
    7783.floating-panel {
    78     padding: 4px;
     84    padding: 6px;
    7985    background-color: rgba(255, 255, 255, 0.9);
    8086    border: 1px solid silver;
     
    138144
    139145#options-menu {
    140     border: 1px solid;
     146    border: 1px solid gray;
     147    border-radius: 4px;
    141148    margin-top: 1px;
    142149    padding: 2px 4px;
    143     box-shadow: 2px 2px 2px #888;
    144     -webkit-transition: opacity .2s;
     150    box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.6);
     151    transition: opacity .2s;
    145152    text-align: left;
    146153    position: absolute;
     
    230237    testRunner.dumpAsText();
    231238
    232 var g_state;
    233 function globalState()
     239class Utils
    234240{
    235     if (!g_state) {
    236         g_state = {
    237             crashTests: [],
    238             crashOther: [],
    239             flakyPassTests: [],
    240             hasHttpTests: false,
    241             hasImageFailures: false,
    242             hasTextFailures: false,
    243             missingResults: [],
    244             results: {},
    245             shouldToggleImages: true,
    246             failingTests: [],
    247             testsWithStderr: [],
    248             timeoutTests: [],
    249             unexpectedPassTests: []
    250         }
    251     }
    252     return g_state;
    253 }
    254 
     241    static matchesSelector(node, selector)
     242    {
     243        if (node.matches)
     244            return node.matches(selector);
     245
     246        if (node.webkitMatchesSelector)
     247            return node.webkitMatchesSelector(selector);
     248
     249        if (node.mozMatchesSelector)
     250            return node.mozMatchesSelector(selector);
     251    }
     252
     253    static parentOfType(node, selector)
     254    {
     255        while (node = node.parentNode) {
     256            if (Utils.matchesSelector(node, selector))
     257                return node;
     258        }
     259        return null;
     260    }
     261
     262    static stripExtension(testName)
     263    {
     264        // Temporary fix, also in Tools/Scripts/webkitpy/layout_tests/constrollers/test_result_writer.py, line 95.
     265        // FIXME: Refactor to avoid confusing reference to both test and process names.
     266        if (Utils.splitExtension(testName)[1].length > 5)
     267            return testName;
     268        return Utils.splitExtension(testName)[0];
     269    }
     270
     271    static splitExtension(testName)
     272    {
     273        let index = testName.lastIndexOf('.');
     274        if (index == -1) {
     275            return [testName, ''];
     276        }
     277        return [testName.substring(0, index), testName.substring(index + 1)];
     278    }
     279
     280    static forEach(nodeList, handler)
     281    {
     282        Array.prototype.forEach.call(nodeList, handler);
     283    }
     284
     285    static toArray(nodeList)
     286    {
     287        return Array.prototype.slice.call(nodeList);
     288    }
     289
     290    static trim(string)
     291    {
     292        return string.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
     293    }
     294
     295    static async(func, args)
     296    {
     297        setTimeout(() => { func.apply(null, args); }, 50);
     298    }
     299
     300    static appendHTML(node, html)
     301    {
     302        if (node.insertAdjacentHTML)
     303            node.insertAdjacentHTML('beforeEnd', html);
     304        else
     305            node.innerHTML += html;
     306    }};
     307
     308class TestResult
     309{
     310    constructor(info, name)
     311    {
     312        this.name = name;
     313        this.info = info; // FIXME: make this private.
     314    }
     315
     316    isFailureExpected()
     317    {
     318        let actual = this.info.actual;   
     319        let expected = this.info.expected || 'PASS';
     320
     321        if (actual != 'SKIP') {
     322            let expectedArray = expected.split(' ');
     323            let actualArray = actual.split(' ');
     324            for (let actualValue of actualArray) {
     325                if (expectedArray.indexOf(actualValue) == -1 && (expectedArray.indexOf('FAIL') == -1 || (actualValue != 'TEXT' && actualValue != 'IMAGE+TEXT' && actualValue != 'AUDIO')))
     326                    return false;
     327            }
     328        }
     329        return true;
     330    }
     331   
     332    isMissing()
     333    {
     334        return this.info.actual.indexOf('MISSING') != -1;
     335    }
     336   
     337    isFlakey(pixelTestsEnabled)
     338    {
     339        let actualTokens = this.info.actual.split(' ');
     340        let passedWithImageOnlyFailureInRetry = actualTokens[0] == 'TEXT' && actualTokens[1] == 'IMAGE';
     341        if (actualTokens[1] && this.info.actual.indexOf('PASS') != -1 || (!pixelTestsEnabled && passedWithImageOnlyFailureInRetry))
     342            return true;
     343       
     344        return false;
     345    }
     346   
     347    isPass()
     348    {
     349        return this.info.actual == 'PASS';
     350    }
     351
     352    isTextFailure()
     353    {
     354        return this.info.actual.indexOf('TEXT') != -1;
     355    }
     356
     357    isImageFailure()
     358    {
     359        return this.info.actual.indexOf('IMAGE') != -1;
     360    }
     361
     362    isAudioFailure()
     363    {
     364        return this.info.actual.indexOf('AUDIO') != -1;
     365    }
     366
     367    isCrash()
     368    {
     369        return this.info.actual == 'CRASH';
     370    }
     371   
     372    isTimeout()
     373    {
     374        return this.info.actual == 'TIMEOUT';
     375    }
     376   
     377    isUnexpectedPass(pixelTestsEnabled)
     378    {
     379        if (this.info.actual == 'PASS' && this.info.expected != 'PASS') {
     380            if (this.info.expected != 'IMAGE' || (pixelTestsEnabled || this.isRefTest()))
     381                return true;
     382        }
     383       
     384        return false;
     385    }
     386   
     387    isRefTest()
     388    {
     389        return !!this.info.reftest_type;
     390    }
     391
     392    isMismatchRefTest()
     393    {
     394        return this.isRefTest() && this.info.reftest_type.indexOf('!=') != -1;
     395    }
     396
     397    isMatchRefTest()
     398    {
     399        return this.isRefTest() && this.info.reftest_type.indexOf('==') != -1;
     400    }
     401   
     402    isMissingImage()
     403    {
     404        return this.info.is_missing_image;
     405    }
     406   
     407    hasStdErr()
     408    {
     409        return this.info.has_stderr;
     410    }
     411};
     412
     413class TestResults
     414{
     415    constructor(results)
     416    {
     417        this._results = results;
     418
     419        this.crashTests = [];
     420        this.crashOther = [];
     421        this.missingResults = [];
     422        this.failingTests = [];
     423        this.testsWithStderr = [];
     424        this.timeoutTests = [];
     425        this.unexpectedPassTests = [];
     426        this.flakyPassTests = [];
     427
     428        this.hasHttpTests = false;
     429        this.hasImageFailures = false;
     430        this.hasTextFailures = false;
     431
     432        this._forEachTest(this._results.tests, '');
     433        this._forOtherCrashes(this._results.other_crashes);
     434    }
     435   
     436    date()
     437    {
     438        return this._results.date;
     439    }
     440
     441    layoutTestsDir()
     442    {
     443        return this._results.layout_tests_dir;
     444    }
     445   
     446    usesExpectationsFile()
     447    {
     448        return this._results.uses_expectations_file;
     449    }
     450   
     451    resultForTest(testName)
     452    {
     453        return this._resultsByTest[testName];
     454    }
     455   
     456    wasInterrupted()
     457    {
     458        return this._results.interrupted;
     459    }
     460
     461    hasPrettyPatch()
     462    {
     463        return this._results.has_pretty_patch;
     464    }
     465   
     466    hasWDiff()
     467    {
     468        return this._results.has_wdiff;
     469    }
     470
     471    _processResultForTest(testResult)
     472    {
     473        let test = testResult.name;
     474        if (testResult.hasStdErr())
     475            this.testsWithStderr.push(testResult);
     476
     477        this.hasHttpTests |= test.indexOf('http/') == 0;
     478
     479        if (this.usesExpectationsFile())
     480            testResult.isExpected = testResult.isFailureExpected();
     481       
     482        if (testResult.isTextFailure())
     483            this.hasTextFailures = true;
     484
     485        if (testResult.isImageFailure())
     486            this.hasImageFailures = true;
     487
     488        if (testResult.isMissing()) {
     489            // FIXME: make sure that new-run-webkit-tests spits out an -actual.txt file for tests with MISSING results.
     490            this.missingResults.push(testResult);
     491            return;
     492        }
     493
     494        if (testResult.isFlakey(this._results.pixel_tests_enabled)) {
     495            this.flakyPassTests.push(testResult);
     496            return;
     497        }
     498
     499        if (testResult.isPass()) {
     500            if (testResult.isUnexpectedPass(this._results.pixel_tests_enabled))
     501                this.unexpectedPassTests.push(testResult);
     502            return;
     503        }
     504
     505        if (testResult.isCrash()) {
     506            this.crashTests.push(testResult);
     507            return;
     508        }
     509
     510        if (testResult.isTimeout()) {
     511            this.timeoutTests.push(testResult);
     512            return;
     513        }
     514   
     515        this.failingTests.push(testResult);
     516    }
     517   
     518    _forEachTest(tree, prefix)
     519    {
     520        for (let key in tree) {
     521            let newPrefix = prefix ? (prefix + '/' + key) : key;
     522            if ('actual' in tree[key]) {
     523                let testObject = new TestResult(tree[key], newPrefix);
     524                this._processResultForTest(testObject);
     525            } else
     526                this._forEachTest(tree[key], newPrefix);
     527        }
     528    }
     529
     530    _forOtherCrashes(tree)
     531    {
     532        for (let key in tree) {
     533            let testObject = new TestResult(tree[key], key);
     534            this.crashOther.push(testObject);
     535        }
     536    }
     537   
     538    static sortByName(tests)
     539    {
     540        tests.sort(function (a, b) { return a.name.localeCompare(b.name) });
     541    }
     542
     543    static hasUnexpectedResult(tests)
     544    {
     545        return tests.some(function (test) { return !test.isExpected; });
     546    }
     547};
     548
     549class TestResultsController
     550{       
     551    constructor(containerElement, testResults)
     552    {
     553        this.containerElement = containerElement;
     554        this.testResults = testResults;
     555
     556        this.shouldToggleImages = true;
     557        this._togglingImageInterval = null;
     558       
     559        this._updatePageTitle();
     560
     561        this.buildResultsTables();
     562        this.hideNonApplicableUI();
     563        this.setupSorting();
     564        this.setupOptions();
     565    }
     566   
     567    buildResultsTables()
     568    {
     569        if (this.testResults.wasInterrupted()) {
     570            let interruptionMessage = document.createElement('p');
     571            interruptionMessage.textContent = 'Testing exited early';
     572            interruptionMessage.classList.add('stopped-running-early-message');
     573            this.containerElement.appendChild(interruptionMessage);
     574        }
     575
     576        if (this.testResults.crashTests.length)
     577            this.containerElement.appendChild(this.buildOneSection(this.testResults.crashTests, CrashingTestsSectionBuilder));
     578
     579        if (this.testResults.crashOther.length)
     580            this.containerElement.appendChild(this.buildOneSection(this.testResults.crashOther, OtherCrashesSectionBuilder));
     581
     582        if (this.testResults.failingTests.length)
     583            this.containerElement.appendChild(this.buildOneSection(this.testResults.failingTests, FailingTestsSectionBuilder));
     584
     585        if (this.testResults.missingResults.length)
     586            this.containerElement.appendChild(this.buildOneSection(this.testResults.missingResults, TestsWithMissingResultsSectionBuilder));
     587
     588        if (this.testResults.timeoutTests.length)
     589            this.containerElement.appendChild(this.buildOneSection(this.testResults.timeoutTests, TimedOutTestsSectionBuilder));
     590
     591        if (this.testResults.testsWithStderr.length)
     592            this.containerElement.appendChild(this.buildOneSection(this.testResults.testsWithStderr, TestsWithStdErrSectionBuilder));
     593
     594        if (this.testResults.flakyPassTests.length)
     595            this.containerElement.appendChild(this.buildOneSection(this.testResults.flakyPassTests, FlakyPassTestsSectionBuilder));
     596
     597        if (this.testResults.usesExpectationsFile() && this.testResults.unexpectedPassTests.length)
     598            this.containerElement.appendChild(this.buildOneSection(this.testResults.unexpectedPassTests, UnexpectedPassTestsSectionBuilder));
     599
     600        if (this.testResults.hasHttpTests) {
     601            let httpdAccessLogLink = document.createElement('p');
     602            httpdAccessLogLink.innerHTML = 'httpd access log: <a href="access_log.txt">access_log.txt</a>';
     603
     604            let httpdErrorLogLink = document.createElement('p');
     605            httpdErrorLogLink.innerHTML = 'httpd error log: <a href="error_log.txt">error_log.txt</a>';
     606           
     607            this.containerElement.appendChild(httpdAccessLogLink);
     608            this.containerElement.appendChild(httpdErrorLogLink);
     609        }
     610       
     611        this.updateTestlistCounts();
     612    }
     613   
     614    setupSorting()
     615    {
     616        let resultsTable = document.getElementById('results-table');
     617        if (!resultsTable)
     618            return;
     619       
     620        // FIXME: Make all the tables sortable. Maybe SectionBuilder should put a TableSorter on each table.
     621        resultsTable.addEventListener('click', TableSorter.handleClick, false);
     622        TableSorter.sortColumn(0);
     623    }
     624   
     625    hideNonApplicableUI()
     626    {
     627        // FIXME: do this all through body classnames.
     628        if (!this.testResults.hasTextFailures) {
     629            let textResultsHeader = document.getElementById('text-results-header');
     630            if (textResultsHeader)
     631                textResultsHeader.textContent = '';
     632        }
     633
     634        if (!this.testResults.hasImageFailures) {
     635            let imageResultsHeader = document.getElementById('image-results-header');
     636            if (imageResultsHeader)
     637                imageResultsHeader.textContent = '';
     638
     639            Utils.parentOfType(document.getElementById('toggle-images'), 'label').style.display = 'none';
     640        }
     641    }
     642   
     643    setupOptions()
     644    {
     645        // FIXME: do this all through body classnames.
     646        if (!this.testResults.usesExpectationsFile())
     647            Utils.parentOfType(document.getElementById('unexpected-results'), 'label').style.display = 'none';
     648    }
     649
     650    buildOneSection(tests, sectionBuilderClass)
     651    {
     652        TestResults.sortByName(tests);
     653       
     654        let sectionBuilder = new sectionBuilderClass(tests, this);
     655        return sectionBuilder.build();
     656    }
     657
     658    updateTestlistCounts()
     659    {
     660        // FIXME: do this through the data model, not through the DOM.
     661        let onlyShowUnexpectedFailures = this.onlyShowUnexpectedFailures();
     662        Utils.forEach(document.querySelectorAll('.test-list-count'), count => {
     663            let container = Utils.parentOfType(count, 'section');
     664            let testContainers;
     665            if (onlyShowUnexpectedFailures)
     666                testContainers = container.querySelectorAll('tbody:not(.expected)');
     667            else
     668                testContainers = container.querySelectorAll('tbody');
     669
     670            count.textContent = testContainers.length;
     671        })
     672    }
     673   
     674    flagAll(headerLink)
     675    {
     676        let tests = this.visibleTests(Utils.parentOfType(headerLink, 'section'));
     677        Utils.forEach(tests, tests => {
     678            let shouldFlag = true;
     679            testNavigator.flagTest(tests, shouldFlag);
     680        })
     681    }
     682
     683    unflag(flag)
     684    {
     685        const shouldFlag = false;
     686        testNavigator.flagTest(Utils.parentOfType(flag, 'tbody'), shouldFlag);
     687    }
     688
     689    visibleTests(opt_container)
     690    {
     691        let container = opt_container || document;
     692        if (this.onlyShowUnexpectedFailures())
     693            return container.querySelectorAll('tbody:not(.expected)');
     694        else
     695            return container.querySelectorAll('tbody');
     696    }
     697
     698    // FIXME: this is confusing. Flip the sense around.
     699    onlyShowUnexpectedFailures()
     700    {
     701        return document.getElementById('unexpected-results').checked;
     702    }
     703
     704    static _testListHeader(title)
     705    {
     706        let header = document.createElement('h1');
     707        header.innerHTML = title + ' (<span class=test-list-count></span>): <a href="#" class=flag-all onclick="controller.flagAll(this)">flag all</a>';
     708        return header;
     709    }
     710
     711    testToURL(testResult, layoutTestsPath)
     712    {
     713        const mappings = {
     714            "http/tests/ssl/": "https://127.0.0.1:8443/ssl/",
     715            "http/tests/": "http://127.0.0.1:8000/",
     716            "http/wpt/": "http://localhost:8800/WebKit/",
     717            "imported/w3c/web-platform-tests/": "http://localhost:8800/"
     718        };
     719
     720        for (let key in mappings) {
     721            if (testResult.name.startsWith(key))
     722                return mappings[key] + testResult.name.substring(key.length);
     723
     724        }
     725        return "file://" + layoutTestsPath + "/" + testResult.name;
     726    }
     727
     728    layoutTestURL(testResult)
     729    {
     730        if (this.shouldUseTracLinks())
     731            return this.layoutTestsBasePath() + testResult.name;
     732
     733        return this.testToURL(testResult, this.layoutTestsBasePath());
     734    }
     735
     736    layoutTestsBasePath()
     737    {
     738        let basePath;
     739        if (this.shouldUseTracLinks()) {
     740            let revision = this.testResults.revision;
     741            basePath = 'http://trac.webkit.org';
     742            basePath += revision ? ('/export/' + revision) : '/browser';
     743            basePath += '/trunk/LayoutTests/';
     744        } else
     745            basePath = this.testResults.layoutTestsDir() + '/';
     746
     747        return basePath;
     748    }
     749
     750    shouldUseTracLinks()
     751    {
     752        return !this.testResults.layoutTestsDir() || !location.toString().indexOf('file://') == 0;
     753    }
     754
     755    checkServerIsRunning(event)
     756    {
     757        if (this.shouldUseTracLinks())
     758            return;
     759
     760        let url = event.target.href;
     761        if (url.startsWith("file://"))
     762            return;
     763
     764        event.preventDefault();
     765        fetch(url, { mode: "no-cors" }).then(() => {
     766            window.location = url;
     767        }, () => {
     768            alert("HTTP server does not seem to be running, please use the run-webkit-httpd script");
     769        });
     770    }
     771
     772    testLink(testResult)
     773    {
     774        return '<a class=test-link onclick="controller.checkServerIsRunning(event)" href="' + this.layoutTestURL(testResult) + '">' + testResult.name + '</a><span class=flag onclick="controller.unflag(this)"> \u2691</span>';
     775    }
     776   
     777    static resultLink(testPrefix, suffix, contents)
     778    {
     779        return '<a class=result-link href="' + testPrefix + suffix + '" data-prefix="' + testPrefix + '">' + contents + '</a> ';
     780    }
     781
     782    textResultLinks(prefix)
     783    {
     784        let html = TestResultsController.resultLink(prefix, '-expected.txt', 'expected') +
     785            TestResultsController.resultLink(prefix, '-actual.txt', 'actual') +
     786            TestResultsController.resultLink(prefix, '-diff.txt', 'diff');
     787
     788        if (this.testResults.hasPrettyPatch())
     789            html += TestResultsController.resultLink(prefix, '-pretty-diff.html', 'pretty diff');
     790
     791        if (this.testResults.hasWDiff())
     792            html += TestResultsController.resultLink(prefix, '-wdiff.html', 'wdiff');
     793
     794        return html;
     795    }
     796
     797    flakinessDashboardURLForTests(testObjects)
     798    {
     799        // FIXME: just map and join here.
     800        let testList = '';
     801        for (let i = 0; i < testObjects.length; ++i) {
     802            testList += testObjects[i].name;
     803
     804            if (i != testObjects.length - 1)
     805                testList += ',';
     806        }
     807
     808        return 'http://webkit-test-results.webkit.org/dashboards/flakiness_dashboard.html#showAllRuns=true&tests=' + encodeURIComponent(testList);
     809    }
     810
     811    _updatePageTitle()
     812    {
     813        let dateString = this.testResults.date();
     814        let title = document.createElement('title');
     815        title.textContent = 'Layout Test Results from ' + dateString;
     816        document.head.appendChild(title);
     817    }
     818   
     819    // Options handling. FIXME: move to a separate class?
     820    updateAllOptions()
     821    {
     822        Utils.forEach(document.querySelectorAll('#options-menu input'), input => { input.onchange() });
     823    }
     824
     825    toggleOptionsMenu()
     826    {
     827        let menu = document.getElementById('options-menu');
     828        menu.className = (menu.className == 'hidden-menu') ? '' : 'hidden-menu';
     829    }
     830
     831    handleToggleUseNewlines()
     832    {
     833        OptionWriter.save();
     834        testNavigator.updateFlaggedTests();
     835    }
     836
     837    handleUnexpectedResultsChange()
     838    {
     839        OptionWriter.save();
     840        this._updateExpectedFailures();
     841    }
     842
     843    expandAllExpectations()
     844    {
     845        let expandLinks = this._visibleExpandLinks();
     846        for (let link of expandLinks)
     847            Utils.async(link => { controller.expandExpectations(link) }, [ link ]);
     848    }
     849
     850    collapseAllExpectations()
     851    {
     852        let expandLinks = this._visibleExpandLinks();
     853        for (let link of expandLinks)
     854            Utils.async(link => { controller.collapseExpectations(link) }, [ link ]);
     855    }
     856
     857    expandExpectations(expandLink)
     858    {
     859        let row = Utils.parentOfType(expandLink, 'tr');
     860        let parentTbody = row.parentNode;
     861        let existingResultsRow = parentTbody.querySelector('.results-row');
     862   
     863        const enDash = '\u2013';
     864        expandLink.textContent = enDash;
     865        if (existingResultsRow) {
     866            this._updateExpandedState(existingResultsRow, true);
     867            return;
     868        }
     869   
     870        let newRow = document.createElement('tr');
     871        newRow.className = 'results-row';
     872        let newCell = document.createElement('td');
     873        newCell.colSpan = row.querySelectorAll('td').length;
     874
     875        let resultLinks = row.querySelectorAll('.result-link');
     876        let hasTogglingImages = false;
     877        for (let link of resultLinks) {
     878            let result;
     879            if (link.textContent == 'images') {
     880                hasTogglingImages = true;
     881                result = TestResultsController._togglingImage(link.getAttribute('data-prefix'));
     882            } else
     883                result = TestResultsController._resultIframe(link.href);
     884
     885            Utils.appendHTML(newCell, result);   
     886        }
     887
     888        newRow.appendChild(newCell);
     889        parentTbody.appendChild(newRow);
     890
     891        this._updateExpandedState(newRow, true);
     892
     893        this._updateImageTogglingTimer();
     894    }
     895
     896    collapseExpectations(expandLink)
     897    {
     898        expandLink.textContent = '+';
     899        let existingResultsRow = Utils.parentOfType(expandLink, 'tbody').querySelector('.results-row');
     900        if (existingResultsRow)
     901            this._updateExpandedState(existingResultsRow, false);
     902    }
     903
     904    toggleExpectations(element)
     905    {
     906        let expandLink = element;
     907        if (expandLink.className != 'expand-button-text')
     908            expandLink = expandLink.querySelector('.expand-button-text');
     909
     910        if (expandLink.textContent == '+')
     911            this.expandExpectations(expandLink);
     912        else
     913            this.collapseExpectations(expandLink);
     914    }
     915
     916    _updateExpandedState(row, isExpanded)
     917    {
     918        row.setAttribute('data-expanded', isExpanded);
     919        this._updateImageTogglingTimer();
     920    }
     921
     922    handleToggleImagesChange()
     923    {
     924        OptionWriter.save();
     925        this._updateTogglingImages();
     926    }
     927
     928    _visibleExpandLinks()
     929    {
     930        if (this.onlyShowUnexpectedFailures())
     931            return document.querySelectorAll('tbody:not(.expected) .expand-button-text');
     932        else
     933            return document.querySelectorAll('.expand-button-text');
     934    }
     935
     936    static _togglingImage(prefix)
     937    {
     938        return '<div class=result-container><div class="label imageText"></div><img class=animatedImage data-prefix="' + prefix + '"></img></div>';
     939    }
     940
     941    _updateTogglingImages()
     942    {
     943        this.shouldToggleImages = document.getElementById('toggle-images').checked;
     944
     945        // FIXME: this is all pretty confusing. Simplify.
     946        if (this.shouldToggleImages) {
     947            Utils.forEach(document.querySelectorAll('table:not(#missing-table) tbody:not([mismatchreftest]) a[href$=".png"]'), TestResultsController._convertToTogglingHandler(function(prefix) {
     948                return TestResultsController.resultLink(prefix, '-diffs.html', 'images');
     949            }));
     950            Utils.forEach(document.querySelectorAll('table:not(#missing-table) tbody:not([mismatchreftest]) img[src$=".png"]'), TestResultsController._convertToTogglingHandler(TestResultsController._togglingImage));
     951        } else {
     952            Utils.forEach(document.querySelectorAll('a[href$="-diffs.html"]'), element => {
     953                TestResultsController._convertToNonTogglingHandler(element);
     954            });
     955            Utils.forEach(document.querySelectorAll('.animatedImage'), TestResultsController._convertToNonTogglingHandler(function (absolutePrefix, suffix) {
     956                return TestResultsController._resultIframe(absolutePrefix + suffix);
     957            }));
     958        }
     959
     960        this._updateImageTogglingTimer();
     961    }
     962
     963    _updateExpectedFailures()
     964    {
     965        // Gross to do this by setting stylesheet text. Use a body class!
     966        document.getElementById('unexpected-style').textContent = this.onlyShowUnexpectedFailures() ? '.expected { display: none; }' : '';
     967
     968        this.updateTestlistCounts();
     969        testNavigator.onlyShowUnexpectedFailuresChanged();
     970    }
     971
     972    static _resultIframe(src)
     973    {
     974        // FIXME: use audio tags for AUDIO tests?
     975        let layoutTestsIndex = src.indexOf('LayoutTests');
     976        let name;
     977        if (layoutTestsIndex != -1) {
     978            let hasTrac = src.indexOf('trac.webkit.org') != -1;
     979            let prefix = hasTrac ? 'trac.webkit.org/.../' : '';
     980            name = prefix + src.substring(layoutTestsIndex + 'LayoutTests/'.length);
     981        } else {
     982            let lastDashIndex = src.lastIndexOf('-pretty');
     983            if (lastDashIndex == -1)
     984                lastDashIndex = src.lastIndexOf('-');
     985            name = src.substring(lastDashIndex + 1);
     986        }
     987
     988        let tagName = (src.lastIndexOf('.png') == -1) ? 'iframe' : 'img';
     989
     990        if (tagName != 'img')
     991            src += '?format=txt';
     992        return '<div class=result-container><div class=label>' + name + '</div><' + tagName + ' src="' + src + '"></' + tagName + '></div>';
     993    }
     994
     995
     996    static _toggleImages()
     997    {
     998        let images = document.querySelectorAll('.animatedImage');
     999        let imageTexts = document.querySelectorAll('.imageText');
     1000        for (let i = 0, len = images.length; i < len; i++) {
     1001            let image = images[i];
     1002            let text = imageTexts[i];
     1003            if (text.textContent == 'Expected Image') {
     1004                text.textContent = 'Actual Image';
     1005                image.src = image.getAttribute('data-prefix') + '-actual.png';
     1006            } else {
     1007                text.textContent = 'Expected Image';
     1008                image.src = image.getAttribute('data-prefix') + '-expected.png';
     1009            }
     1010        }
     1011    }
     1012
     1013    _updateImageTogglingTimer()
     1014    {
     1015        let hasVisibleAnimatedImage = document.querySelector('.results-row[data-expanded="true"] .animatedImage');
     1016        if (!hasVisibleAnimatedImage) {
     1017            clearInterval(this._togglingImageInterval);
     1018            this._togglingImageInterval = null;
     1019            return;
     1020        }
     1021
     1022        if (!this._togglingImageInterval) {
     1023            TestResultsController._toggleImages();
     1024            this._togglingImageInterval = setInterval(TestResultsController._toggleImages, 2000);
     1025        }
     1026    }
     1027   
     1028    static _getResultContainer(node)
     1029    {
     1030        return (node.tagName == 'IMG') ? Utils.parentOfType(node, '.result-container') : node;
     1031    }
     1032
     1033    static _convertToTogglingHandler(togglingImageFunction)
     1034    {
     1035        return function(node) {
     1036            let url = (node.tagName == 'IMG') ? node.src : node.href;
     1037            if (url.match('-expected.png$'))
     1038                TestResultsController._getResultContainer(node).remove();
     1039            else if (url.match('-actual.png$')) {
     1040                let name = Utils.parentOfType(node, 'tbody').querySelector('.test-link').textContent;
     1041                TestResultsController._getResultContainer(node).outerHTML = togglingImageFunction(Utils.stripExtension(name));
     1042            }
     1043        }
     1044    }
     1045   
     1046    static _convertToNonTogglingHandler(resultFunction)
     1047    {
     1048        return function(node) {
     1049            let prefix = node.getAttribute('data-prefix');
     1050            TestResultsController._getResultContainer(node).outerHTML = resultFunction(prefix, '-expected.png', 'expected') + resultFunction(prefix, '-actual.png', 'actual');
     1051        }
     1052    }
     1053};
     1054
     1055class SectionBuilder {
     1056   
     1057    constructor(tests, resultsController)
     1058    {
     1059        this._tests = tests;
     1060        this._table = null;
     1061        this._resultsController = resultsController;
     1062    }
     1063
     1064    build()
     1065    {
     1066        TestResults.sortByName(this._tests);
     1067       
     1068        let section = document.createElement('section');
     1069        section.appendChild(TestResultsController._testListHeader(this.sectionTitle()));
     1070        if (this.hideWhenShowingUnexpectedResultsOnly())
     1071            section.classList.add('expected');
     1072
     1073        this._table = document.createElement('table');
     1074        this._table.id = this.tableID();
     1075        this.addTableHeader();
     1076
     1077        let visibleResultsCount = 0;
     1078        for (let testResult of this._tests) {
     1079            let tbody = this.createTableRow(testResult);
     1080            this._table.appendChild(tbody);
     1081           
     1082            if (!this._resultsController.onlyShowUnexpectedFailures() || testResult.isExpected)
     1083                ++visibleResultsCount;
     1084        }
     1085       
     1086        section.querySelector('.test-list-count').textContent = visibleResultsCount;
     1087        section.appendChild(this._table);
     1088        return section;
     1089    }
     1090
     1091    createTableRow(testResult)
     1092    {
     1093        let tbody = document.createElement('tbody');
     1094        if (testResult.isExpected)
     1095            tbody.classList.add('expected');
     1096       
     1097        let row = document.createElement('tr');
     1098        tbody.appendChild(row);
     1099       
     1100        let testNameCell = document.createElement('td');
     1101        this.fillTestCell(testResult, testNameCell);
     1102        row.appendChild(testNameCell);
     1103
     1104        let resultCell = document.createElement('td');
     1105        this.fillTestResultCell(testResult, resultCell);
     1106        row.appendChild(resultCell);
     1107
     1108        let historyCell = this.createHistoryCell(testResult);
     1109        if (historyCell)
     1110            row.appendChild(historyCell);
     1111
     1112        return tbody;
     1113    }
     1114   
     1115    hideWhenShowingUnexpectedResultsOnly()
     1116    {
     1117        return !TestResults.hasUnexpectedResult(this._tests);
     1118    }
     1119   
     1120    addTableHeader()
     1121    {
     1122    }
     1123   
     1124    fillTestCell(testResult, cell)
     1125    {
     1126        cell.innerHTML = '<span class=expand-button onclick="controller.toggleExpectations(this)"><span class=expand-button-text>+</span></span>' + this._resultsController.testLink(testResult);
     1127    }
     1128
     1129    fillTestResultCell(testResult, cell)
     1130    {
     1131    }
     1132   
     1133    createHistoryCell(testResult)
     1134    {
     1135        let historyCell = document.createElement('td');
     1136        historyCell.innerHTML = '<a href="' + this._resultsController.flakinessDashboardURLForTests([testResult]) + '">history</a>'
     1137        return historyCell;
     1138    }
     1139   
     1140    tableID() { return ''; }
     1141    sectionTitle() { return ''; }
     1142};
     1143
     1144class FailuresSectionBuilder extends SectionBuilder {
     1145   
     1146    addTableHeader()
     1147    {
     1148        let header = document.createElement('thead');
     1149        let html = '<th>test</th><th id="text-results-header">results</th><th id="image-results-header">image results</th>';
     1150
     1151        if (this._resultsController.testResults.usesExpectationsFile())
     1152            html += '<th>actual failure</th><th>expected failure</th>';
     1153
     1154        html += '<th><a href="' + this._resultsController.flakinessDashboardURLForTests(this._tests) + '">history</a></th>';
     1155
     1156        if (this.tableID() == 'flaky-tests-table') // FIXME: use the classes, Luke!
     1157            html += '<th>failures</th>';
     1158
     1159        header.innerHTML = html;
     1160        this._table.appendChild(header);
     1161    }
     1162   
     1163    createTableRow(testResult)
     1164    {
     1165        let tbody = document.createElement('tbody');
     1166        if (testResult.isExpected)
     1167            tbody.classList.add('expected');
     1168       
     1169        if (testResult.isMismatchRefTest())
     1170            tbody.setAttribute('mismatchreftest', 'true');
     1171
     1172        let row = document.createElement('tr');
     1173        tbody.appendChild(row);
     1174       
     1175        let testNameCell = document.createElement('td');
     1176        this.fillTestCell(testResult, testNameCell);
     1177        row.appendChild(testNameCell);
     1178
     1179        let resultCell = document.createElement('td');
     1180        this.fillTestResultCell(testResult, resultCell);
     1181        row.appendChild(resultCell);
     1182
     1183        if (testResult.isTextFailure())
     1184            this.appendTextFailureLinks(testResult, resultCell);
     1185
     1186        if (testResult.isAudioFailure())
     1187            this.appendAudioFailureLinks(testResult, resultCell);
     1188           
     1189        if (testResult.isMissing())
     1190            this.appendActualOnlyLinks(testResult, resultCell);
     1191
     1192        let actualTokens = testResult.info.actual.split(/\s+/);
     1193
     1194        let testPrefix = Utils.stripExtension(testResult.name);
     1195        let imageResults = this.imageResultLinks(testResult, testPrefix, actualTokens[0]);
     1196        if (!imageResults && actualTokens.length > 1)
     1197            imageResults = this.imageResultLinks(testResult, 'retries/' + testPrefix, actualTokens[1]);
     1198
     1199        let imageResultsCell = document.createElement('td');
     1200        imageResultsCell.innerHTML = imageResults;
     1201        row.appendChild(imageResultsCell);
     1202
     1203        if (this._resultsController.testResults.usesExpectationsFile() || actualTokens.length) {
     1204            let actualCell = document.createElement('td');
     1205            actualCell.textContent = testResult.info.actual;
     1206            row.appendChild(actualCell);
     1207        }
     1208
     1209        if (this._resultsController.testResults.usesExpectationsFile()) {
     1210            let expectedCell = document.createElement('td');
     1211            expectedCell.textContent = testResult.isMissing() ? '' : testResult.info.expected;
     1212            row.appendChild(expectedCell);
     1213        }
     1214
     1215        let historyCell = this.createHistoryCell(testResult);
     1216        if (historyCell)
     1217            row.appendChild(historyCell);
     1218
     1219        return tbody;
     1220    }
     1221
     1222    appendTextFailureLinks(testResult, cell)
     1223    {
     1224        cell.innerHTML += this._resultsController.textResultLinks(Utils.stripExtension(testResult.name));
     1225    }
     1226   
     1227    appendAudioFailureLinks(testResult, cell)
     1228    {
     1229        let prefix = Utils.stripExtension(testResult.name);
     1230        cell.innerHTML += TestResultsController.resultLink(prefix, '-expected.wav', 'expected audio')
     1231            + TestResultsController.resultLink(prefix, '-actual.wav', 'actual audio')
     1232            + TestResultsController.resultLink(prefix, '-diff.txt', 'textual diff');
     1233    }
     1234   
     1235    appendActualOnlyLinks(testResult, cell)
     1236    {
     1237        if (testResult.info.is_missing_audio)
     1238            cell.innerHTML += TestResultsController.resultLink(prefix, '-actual.wav', 'audio result');
     1239
     1240        if (testResult.info.is_missing_text)
     1241            cell.innerHTML += TestResultsController.resultLink(prefix, '-actual.txt', 'result');
     1242    }
     1243
     1244    imageResultLinks(testResult, testPrefix, resultToken)
     1245    {
     1246        let result = '';
     1247        if (resultToken.indexOf('IMAGE') != -1) {
     1248            let testExtension = Utils.splitExtension(testResult.name)[1];
     1249
     1250            if (testResult.isMismatchRefTest()) {
     1251                result += TestResultsController.resultLink(this._resultsController.layoutTestsBasePath() + testPrefix, '-expected-mismatch.' + testExtension, 'ref mismatch');
     1252                result += TestResultsController.resultLink(testPrefix, '-actual.png', 'actual');
     1253            } else {
     1254                if (testResult.isMatchRefTest())
     1255                    result += TestResultsController.resultLink(this._resultsController.layoutTestsBasePath() + testPrefix, '-expected.' + testExtension, 'reference');
     1256
     1257                if (this._resultsController.shouldToggleImages)
     1258                    result += TestResultsController.resultLink(testPrefix, '-diffs.html', 'images');
     1259                else {
     1260                    result += TestResultsController.resultLink(testPrefix, '-expected.png', 'expected');
     1261                    result += TestResultsController.resultLink(testPrefix, '-actual.png', 'actual');
     1262                }
     1263
     1264                let diff = testResult.info.image_diff_percent;
     1265                result += TestResultsController.resultLink(testPrefix, '-diff.png', 'diff (' + diff + '%)');
     1266            }
     1267        }
     1268       
     1269        if (testResult.isMissing() && testResult.isMissingImage())
     1270            result += TestResultsController.resultLink(testPrefix, '-actual.png', 'png result');
     1271       
     1272        return result;
     1273    }
     1274};
     1275
     1276class FailingTestsSectionBuilder extends FailuresSectionBuilder {
     1277    tableID() { return 'results-table'; }
     1278    sectionTitle() { return 'Tests that failed text/pixel/audio diff'; }
     1279};
     1280
     1281class TestsWithMissingResultsSectionBuilder extends FailuresSectionBuilder {
     1282    tableID() { return 'missing-table'; }
     1283    sectionTitle() { return 'Tests that had no expected results (probably new)'; }
     1284};
     1285
     1286class FlakyPassTestsSectionBuilder extends FailuresSectionBuilder {
     1287    tableID() { return 'flaky-tests-table'; }
     1288    sectionTitle() { return 'Flaky tests (failed the first run and passed on retry)'; }
     1289};
     1290
     1291class UnexpectedPassTestsSectionBuilder extends SectionBuilder {
     1292    tableID() { return 'passes-table'; }
     1293    sectionTitle() { return 'Tests expected to fail but passed'; }
     1294
     1295    addTableHeader()
     1296    {
     1297        let header = document.createElement('thead');
     1298        header.innerHTML = '<th>test</th><th>expected failure</th><th>history</th>';
     1299        this._table.appendChild(header);
     1300    }
     1301   
     1302    fillTestCell(testResult, cell)
     1303    {
     1304        cell.innerHTML = '<a class=test-link onclick="controller.checkServerIsRunning(event)" href="' + this._resultsController.layoutTestURL(testResult) + '">' + testResult.name + '</a><span class=flag onclick="controller.unflag(this)"> \u2691</span>';
     1305    }
     1306
     1307    fillTestResultCell(testResult, cell)
     1308    {
     1309        cell.innerHTML = testResult.info.expected;
     1310    }
     1311};
     1312
     1313
     1314class TestsWithStdErrSectionBuilder extends SectionBuilder {
     1315    tableID() { return 'stderr-table'; }
     1316    sectionTitle() { return 'Tests that had stderr output'; }
     1317    hideWhenShowingUnexpectedResultsOnly() { return false; }
     1318
     1319    fillTestResultCell(testResult, cell)
     1320    {
     1321        cell.innerHTML = TestResultsController.resultLink(Utils.stripExtension(testResult.name), '-stderr.txt', 'stderr');
     1322    }
     1323};
     1324
     1325class TimedOutTestsSectionBuilder extends SectionBuilder {
     1326    tableID() { return 'timeout-tests-table'; }
     1327    sectionTitle() { return 'Tests that timed out'; }
     1328
     1329    fillTestResultCell(testResult, cell)
     1330    {
     1331        // FIXME: only include timeout actual/diff results here if we actually spit out results for timeout tests.
     1332        cell.innerHTML = this._resultsController.textResultLinks(Utils.stripExtension(testResult.name));
     1333    }
     1334};
     1335
     1336class CrashingTestsSectionBuilder extends SectionBuilder {
     1337    tableID() { return 'crash-tests-table'; }
     1338    sectionTitle() { return 'Tests that crashed'; }
     1339
     1340    fillTestResultCell(testResult, cell)
     1341    {
     1342        cell.innerHTML = TestResultsController.resultLink(Utils.stripExtension(testResult.name), '-crash-log.txt', 'crash log')
     1343                       + TestResultsController.resultLink(Utils.stripExtension(testResult.name), '-sample.txt', 'sample');
     1344    }
     1345};
     1346
     1347class OtherCrashesSectionBuilder extends SectionBuilder {
     1348    tableID() { return 'other-crash-tests-table'; }
     1349    sectionTitle() { return 'Other crashes'; }
     1350    fillTestCell(testResult, cell)
     1351    {
     1352        cell.innerHTML = '<span class=expand-button onclick="controller.toggleExpectations(this)"><span class=expand-button-text>+</span></span>' + testResult.name;
     1353    }
     1354
     1355    fillTestResultCell(testResult, cell)
     1356    {
     1357        cell.innerHTML = TestResultsController.resultLink(Utils.stripExtension(testResult.name), '-crash-log.txt', 'crash log');
     1358    }
     1359
     1360    createHistoryCell(testResult)
     1361    {
     1362        return null;
     1363    }
     1364};
     1365
     1366class PixelZoomer {
     1367    constructor()
     1368    {
     1369        this.showOnDelay = true;
     1370        this._zoomFactor = 6;
     1371
     1372        this._resultWidth = 800;
     1373        this._resultHeight = 600;
     1374       
     1375        this._percentX = 0;
     1376        this._percentY = 0;
     1377
     1378        document.addEventListener('mousemove', this, false);
     1379        document.addEventListener('mouseout', this, false);
     1380    }
     1381
     1382    _zoomedResultWidth()
     1383    {
     1384        return this._resultWidth * this._zoomFactor;
     1385    }
     1386   
     1387    _zoomedResultHeight()
     1388    {
     1389        return this._resultHeight * this._zoomFactor;
     1390    }
     1391   
     1392    _zoomImageContainer(url)
     1393    {
     1394        let container = document.createElement('div');
     1395        container.className = 'zoom-image-container';
     1396
     1397        let title = url.match(/\-([^\-]*)\.png/)[1];
     1398   
     1399        let label = document.createElement('div');
     1400        label.className = 'label';
     1401        label.appendChild(document.createTextNode(title));
     1402        container.appendChild(label);
     1403   
     1404        let imageContainer = document.createElement('div');
     1405        imageContainer.className = 'scaled-image-container';
     1406   
     1407        let image = new Image();
     1408        image.src = url;
     1409        image.style.width = this._zoomedResultWidth() + 'px';
     1410        image.style.height = this._zoomedResultHeight() + 'px';
     1411        image.style.border = '1px solid black';
     1412        imageContainer.appendChild(image);
     1413        container.appendChild(imageContainer);
     1414   
     1415        return container;
     1416    }
     1417
     1418    _createContainer(e)
     1419    {
     1420        let tbody = Utils.parentOfType(e.target, 'tbody');
     1421        let row = tbody.querySelector('tr');
     1422        let imageDiffLinks = row.querySelectorAll('a[href$=".png"]');
     1423   
     1424        let container = document.createElement('div');
     1425        container.className = 'pixel-zoom-container';
     1426   
     1427        let html = '';
     1428   
     1429        let togglingImageLink = row.querySelector('a[href$="-diffs.html"]');
     1430        if (togglingImageLink) {
     1431            let prefix = togglingImageLink.getAttribute('data-prefix');
     1432            container.appendChild(this._zoomImageContainer(prefix + '-expected.png'));
     1433            container.appendChild(this._zoomImageContainer(prefix + '-actual.png'));
     1434        }
     1435   
     1436        for (let link of imageDiffLinks)
     1437            container.appendChild(this._zoomImageContainer(link.href));
     1438
     1439        document.body.appendChild(container);
     1440        this._drawAll();
     1441    }
     1442
     1443    _draw(imageContainer)
     1444    {
     1445        let image = imageContainer.querySelector('img');
     1446        let containerBounds = imageContainer.getBoundingClientRect();
     1447        image.style.left = (containerBounds.width / 2 - this._percentX * this._zoomedResultWidth()) + 'px';
     1448        image.style.top = (containerBounds.height / 2 - this._percentY * this._zoomedResultHeight()) + 'px';
     1449    }
     1450
     1451    _drawAll()
     1452    {
     1453        Utils.forEach(document.querySelectorAll('.pixel-zoom-container .scaled-image-container'), element => { this._draw(element) });
     1454    }
     1455   
     1456    handleEvent(event)
     1457    {
     1458        if (event.type == 'mousemove') {
     1459            this._handleMouseMove(event);
     1460            return;
     1461        }
     1462
     1463        if (event.type == 'mouseout') {
     1464            this._handleMouseOut(event);
     1465            return;
     1466        }
     1467    }
     1468
     1469    _handleMouseOut(event)
     1470    {
     1471        if (event.relatedTarget && event.relatedTarget.tagName != 'IFRAME')
     1472            return;
     1473
     1474        // If e.relatedTarget is null, we've moused out of the document.
     1475        let container = document.querySelector('.pixel-zoom-container');
     1476        if (container)
     1477            container.remove();
     1478    }
     1479
     1480    _handleMouseMove(event)
     1481    {
     1482        if (this._mouseMoveTimeout) {
     1483            clearTimeout(this._mouseMoveTimeout);
     1484            this._mouseMoveTimeout = 0;
     1485        }
     1486
     1487        if (Utils.parentOfType(event.target, '.pixel-zoom-container'))
     1488            return;
     1489
     1490        let container = document.querySelector('.pixel-zoom-container');
     1491   
     1492        let resultContainer = (event.target.className == 'result-container') ? event.target : Utils.parentOfType(event.target, '.result-container');
     1493        if (!resultContainer || !resultContainer.querySelector('img')) {
     1494            if (container)
     1495                container.remove();
     1496            return;
     1497        }
     1498
     1499        let targetLocation = event.target.getBoundingClientRect();
     1500        this._percentX = (event.clientX - targetLocation.left) / targetLocation.width;
     1501        this._percentY = (event.clientY - targetLocation.top) / targetLocation.height;
     1502
     1503        if (!container) {
     1504            if (this.showOnDelay) {
     1505                this._mouseMoveTimeout = setTimeout(() => {
     1506                    this._createContainer(event);
     1507                }, 400);
     1508                return;
     1509            }
     1510
     1511            this._createContainer(event);
     1512            return;
     1513        }
     1514   
     1515        this._drawAll();
     1516    }
     1517};
     1518
     1519class TableSorter
     1520{
     1521    static _forwardArrow()
     1522    {
     1523        return '<svg style="width:10px;height:10px"><polygon points="0,0 10,0 5,10" style="fill:#ccc"></svg>';
     1524    }
     1525
     1526    static _backwardArrow()
     1527    {
     1528        return '<svg style="width:10px;height:10px"><polygon points="0,10 10,10 5,0" style="fill:#ccc"></svg>';
     1529    }
     1530
     1531    static _sortedContents(header, arrow)
     1532    {
     1533        return arrow + ' ' + Utils.trim(header.textContent) + ' ' + arrow;
     1534    }
     1535
     1536    static _updateHeaderClassNames(newHeader)
     1537    {
     1538        let sortHeader = document.querySelector('.sortHeader');
     1539        if (sortHeader) {
     1540            if (sortHeader == newHeader) {
     1541                let isAlreadyReversed = sortHeader.classList.contains('reversed');
     1542                if (isAlreadyReversed)
     1543                    sortHeader.classList.remove('reversed');
     1544                else
     1545                    sortHeader.classList.add('reversed');
     1546            } else {
     1547                sortHeader.textContent = sortHeader.textContent;
     1548                sortHeader.classList.remove('sortHeader');
     1549                sortHeader.classList.remove('reversed');
     1550            }
     1551        }
     1552
     1553        newHeader.classList.add('sortHeader');
     1554    }
     1555
     1556    static _textContent(tbodyRow, column)
     1557    {
     1558        return tbodyRow.querySelectorAll('td')[column].textContent;
     1559    }
     1560
     1561    static _sortRows(newHeader, reversed)
     1562    {
     1563        let testsTable = document.getElementById('results-table');
     1564        let headers = Utils.toArray(testsTable.querySelectorAll('th'));
     1565        let sortColumn = headers.indexOf(newHeader);
     1566
     1567        let rows = Utils.toArray(testsTable.querySelectorAll('tbody'));
     1568
     1569        rows.sort(function(a, b) {
     1570            // Only need to support lexicographic sort for now.
     1571            let aText = TableSorter._textContent(a, sortColumn);
     1572            let bText = TableSorter._textContent(b, sortColumn);
     1573       
     1574            // Forward sort equal values by test name.
     1575            if (sortColumn && aText == bText) {
     1576                let aTestName = TableSorter._textContent(a, 0);
     1577                let bTestName = TableSorter._textContent(b, 0);
     1578                if (aTestName == bTestName)
     1579                    return 0;
     1580                return aTestName < bTestName ? -1 : 1;
     1581            }
     1582
     1583            if (reversed)
     1584                return aText < bText ? 1 : -1;
     1585            else
     1586                return aText < bText ? -1 : 1;
     1587        });
     1588
     1589        for (let row of rows)
     1590            testsTable.appendChild(row);
     1591    }
     1592
     1593    static sortColumn(columnNumber)
     1594    {
     1595        let newHeader = document.getElementById('results-table').querySelectorAll('th')[columnNumber];
     1596        TableSorter._sort(newHeader);
     1597    }
     1598
     1599    static handleClick(e)
     1600    {
     1601        let newHeader = e.target;
     1602        if (newHeader.localName != 'th')
     1603            return;
     1604        TableSorter._sort(newHeader);
     1605    }
     1606
     1607    static _sort(newHeader)
     1608    {
     1609        TableSorter._updateHeaderClassNames(newHeader);
     1610   
     1611        let reversed = newHeader.classList.contains('reversed');
     1612        let sortArrow = reversed ? TableSorter._backwardArrow() : TableSorter._forwardArrow();
     1613        newHeader.innerHTML = TableSorter._sortedContents(newHeader, sortArrow);
     1614   
     1615        TableSorter._sortRows(newHeader, reversed);
     1616    }   
     1617};
     1618
     1619class OptionWriter {
     1620    static save()
     1621    {
     1622        let options = document.querySelectorAll('label input');
     1623        let data = {};
     1624        for (let option of options)
     1625            data[option.id] = option.checked;
     1626
     1627        try {
     1628            localStorage.setItem(OptionWriter._key, JSON.stringify(data));
     1629        } catch (err) {
     1630            if (err.name != "SecurityError")
     1631                throw err;
     1632        }
     1633    }
     1634
     1635    static apply()
     1636    {
     1637        let json;
     1638        try {
     1639            json = localStorage.getItem(OptionWriter._key);
     1640        } catch (err) {
     1641           if (err.name != "SecurityError")
     1642              throw err;
     1643        }
     1644
     1645        if (!json) {
     1646            controller.updateAllOptions();
     1647            return;
     1648        }
     1649
     1650        let data = JSON.parse(json);
     1651        for (let id in data) {
     1652            let input = document.getElementById(id);
     1653            if (input)
     1654                input.checked = data[id];
     1655        }
     1656        controller.updateAllOptions();
     1657    }
     1658
     1659    static get _key()
     1660    {
     1661        return 'run-webkit-tests-options';
     1662    }
     1663};
     1664
     1665let testResults;
    2551666function ADD_RESULTS(input)
    2561667{
    257     globalState().results = input;
     1668    testResults = new TestResults(input);
    2581669}
    2591670</script>
     
    2621673
    2631674<script>
    264 function splitExtension(test)
     1675
     1676class TestNavigator
    2651677{
    266     var index = test.lastIndexOf('.');
    267     if (index == -1) {
    268         return [test, ""];
    269     }
    270     return [test.substring(0, index), test.substring(index + 1)];
    271 }
    272 
    273 function stripExtension(test)
    274 {
    275     // Temporary fix, also in Tools/Scripts/webkitpy/layout_tests/constrollers/test_result_writer.py, line 95.
    276     // FIXME: Refactor to avoid confusing reference to both test and process names.
    277     if (splitExtension(test)[1].length > 5)
    278         return test;
    279     return splitExtension(test)[0];
    280 }
    281 
    282 function matchesSelector(node, selector)
    283 {
    284     if (node.matches)
    285         return node.matches(selector);
    286 
    287     if (node.webkitMatchesSelector)
    288         return node.webkitMatchesSelector(selector);
    289 
    290     if (node.mozMatchesSelector)
    291         return node.mozMatchesSelector(selector);
    292 }
    293 
    294 function parentOfType(node, selector)
    295 {
    296     while (node = node.parentNode) {
    297         if (matchesSelector(node, selector))
    298             return node;
    299     }
    300     return null;
    301 }
    302 
    303 function remove(node)
    304 {
    305     node.parentNode.removeChild(node);
    306 }
    307 
    308 function forEach(nodeList, handler)
    309 {
    310     Array.prototype.forEach.call(nodeList, handler);
    311 }
    312 
    313 function resultIframe(src)
    314 {
    315     // FIXME: use audio tags for AUDIO tests?
    316     var layoutTestsIndex = src.indexOf('LayoutTests');
    317     var name;
    318     if (layoutTestsIndex != -1) {
    319         var hasTrac = src.indexOf('trac.webkit.org') != -1;
    320         var prefix = hasTrac ? 'trac.webkit.org/.../' : '';
    321         name = prefix + src.substring(layoutTestsIndex + 'LayoutTests/'.length);
    322     } else {
    323         var lastDashIndex = src.lastIndexOf('-pretty');
    324         if (lastDashIndex == -1)
    325             lastDashIndex = src.lastIndexOf('-');
    326         name = src.substring(lastDashIndex + 1);
    327     }
    328 
    329     var tagName = (src.lastIndexOf('.png') == -1) ? 'iframe' : 'img';
    330 
    331     if (tagName != 'img')
    332         src += '?format=txt';
    333     return '<div class=result-container><div class=label>' + name + '</div><' + tagName + ' src="' + src + '"></' + tagName + '></div>';
    334 }
    335 
    336 function togglingImage(prefix)
    337 {
    338     return '<div class=result-container><div class="label imageText"></div><img class=animatedImage data-prefix="' +
    339         prefix + '"></img></div>';
    340 }
    341 
    342 function toggleExpectations(element)
    343 {
    344     var expandLink = element;
    345     if (expandLink.className != 'expand-button-text')
    346         expandLink = expandLink.querySelector('.expand-button-text');
    347 
    348     if (expandLink.textContent == '+')
    349         expandExpectations(expandLink);
    350     else
    351         collapseExpectations(expandLink);
    352 }
    353 
    354 function collapseExpectations(expandLink)
    355 {
    356     expandLink.textContent = '+';
    357     var existingResultsRow = parentOfType(expandLink, 'tbody').querySelector('.results-row');
    358     if (existingResultsRow)
    359         updateExpandedState(existingResultsRow, false);
    360 }
    361 
    362 function updateExpandedState(row, isExpanded)
    363 {
    364     row.setAttribute('data-expanded', isExpanded);
    365     updateImageTogglingTimer();
    366 }
    367 
    368 function appendHTML(node, html)
    369 {
    370     if (node.insertAdjacentHTML)
    371         node.insertAdjacentHTML('beforeEnd', html);
    372     else
    373         node.innerHTML += html;
    374 }
    375 
    376 function expandExpectations(expandLink)
    377 {
    378     var row = parentOfType(expandLink, 'tr');
    379     var parentTbody = row.parentNode;
    380     var existingResultsRow = parentTbody.querySelector('.results-row');
    381    
    382     var enDash = '\u2013';
    383     expandLink.textContent = enDash;
    384     if (existingResultsRow) {
    385         updateExpandedState(existingResultsRow, true);
    386         return;
    387     }
    388    
    389     var newRow = document.createElement('tr');
    390     newRow.className = 'results-row';
    391     var newCell = document.createElement('td');
    392     newCell.colSpan = row.querySelectorAll('td').length;
    393 
    394     var resultLinks = row.querySelectorAll('.result-link');
    395     var hasTogglingImages = false;
    396     for (var i = 0; i < resultLinks.length; i++) {
    397         var link = resultLinks[i];
    398         var result;
    399         if (link.textContent == 'images') {
    400             hasTogglingImages = true;
    401             result = togglingImage(link.getAttribute('data-prefix'));
    402         } else
    403             result = resultIframe(link.href);
    404 
    405         appendHTML(newCell, result);   
    406     }
    407 
    408     newRow.appendChild(newCell);
    409     parentTbody.appendChild(newRow);
    410 
    411     updateExpandedState(newRow, true);
    412 
    413     updateImageTogglingTimer();
    414 }
    415 
    416 function updateImageTogglingTimer()
    417 {
    418     var hasVisibleAnimatedImage = document.querySelector('.results-row[data-expanded="true"] .animatedImage');
    419     if (!hasVisibleAnimatedImage) {
    420         clearInterval(globalState().togglingImageInterval);
    421         globalState().togglingImageInterval = null;
    422         return;
    423     }
    424 
    425     if (!globalState().togglingImageInterval) {
    426         toggleImages();
    427         globalState().togglingImageInterval = setInterval(toggleImages, 2000);
    428     }
    429 }
    430 
    431 function async(func, args)
    432 {
    433     setTimeout(function() { func.apply(null, args); }, 100);
    434 }
    435 
    436 function visibleTests(opt_container)
    437 {
    438     var container = opt_container || document;
    439     if (onlyShowUnexpectedFailures())
    440         return container.querySelectorAll('tbody:not(.expected)');
    441     else
    442         return container.querySelectorAll('tbody');
    443 }
    444 
    445 function visibleExpandLinks()
    446 {
    447     if (onlyShowUnexpectedFailures())
    448         return document.querySelectorAll('tbody:not(.expected) .expand-button-text');
    449     else
    450         return document.querySelectorAll('.expand-button-text');
    451 }
    452 
    453 function expandAllExpectations()
    454 {
    455     var expandLinks = visibleExpandLinks();
    456     for (var i = 0, len = expandLinks.length; i < len; i++)
    457         async(expandExpectations, [expandLinks[i]]);
    458 }
    459 
    460 function collapseAllExpectations()
    461 {
    462     var expandLinks = visibleExpandLinks();
    463     for (var i = 0, len = expandLinks.length; i < len; i++)
    464         async(collapseExpectations, [expandLinks[i]]);
    465 }
    466 
    467 function shouldUseTracLinks()
    468 {
    469     return !globalState().results.layout_tests_dir || !location.toString().indexOf('file://') == 0;
    470 }
    471 
    472 function layoutTestsBasePath()
    473 {
    474     var basePath;
    475     if (shouldUseTracLinks()) {
    476         var revision = globalState().results.revision;
    477         basePath = 'http://trac.webkit.org';
    478         basePath += revision ? ('/export/' + revision) : '/browser';
    479         basePath += '/trunk/LayoutTests/';
    480     } else
    481         basePath = globalState().results.layout_tests_dir + '/';
    482     return basePath;
    483 }
    484 
    485 var mappings = {
    486     "http/tests/ssl/": "https://127.0.0.1:8443/ssl/",
    487     "http/tests/": "http://127.0.0.1:8000/",
    488     "http/wpt/": "http://localhost:8800/WebKit/",
    489     "imported/w3c/web-platform-tests/": "http://localhost:8800/"
    490 }
    491 
    492 function testToURL(test, layoutTestsPath)
    493 {
    494     for (let key in mappings) {
    495         if (test.startsWith(key))
    496             return mappings[key] + test.substring(key.length);
    497 
    498     }
    499     return "file://" + layoutTestsPath + "/" + test
    500 }
    501 
    502 function layoutTestURL(test)
    503 {
    504     if (shouldUseTracLinks())
    505         return layoutTestsBasePath() + test;
    506     return testToURL(test, layoutTestsBasePath());
    507 }
    508 
    509 function checkServerIsRunning(event)
    510 {
    511     if (shouldUseTracLinks())
    512         return;
    513 
    514     var url = event.target.href;
    515     if (url.startsWith("file://"))
    516         return;
    517 
    518     event.preventDefault();
    519     fetch(url, {mode: "no-cors"}).then(() => {
    520         window.location = url;
    521     }, () => {
    522         alert("HTTP server does not seem to be running, please use the run-webkit-httpd script");
    523     });
    524 }
    525 
    526 function testLink(test)
    527 {
    528     return '<a class=test-link onclick="checkServerIsRunning(event)" href="' + layoutTestURL(test) + '">' + test + '</a><span class=flag onclick="unflag(this)"> \u2691</span>';
    529 }
    530 
    531 function unflag(flag)
    532 {
    533     var shouldFlag = false;
    534     TestNavigator.flagTest(parentOfType(flag, 'tbody'), shouldFlag);
    535 }
    536 
    537 function testLinkWithExpandButton(test)
    538 {
    539     return '<span class=expand-button onclick="toggleExpectations(this)"><span class=expand-button-text>+</span></span>' + testLink(test);
    540 }
    541 
    542 function testWithExpandButton(test)
    543 {
    544     return '<span class=expand-button onclick="toggleExpectations(this)"><span class=expand-button-text>+</span></span>' + test;
    545 }
    546 
    547 function resultLink(testPrefix, suffix, contents)
    548 {
    549     return '<a class=result-link href="' + testPrefix + suffix + '" data-prefix="' + testPrefix + '">' + contents + '</a> ';
    550 }
    551 
    552 function isFailureExpected(expected, actual)
    553 {
    554     var isExpected = true;
    555     if (actual != 'SKIP') {
    556         var expectedArray = expected.split(' ');
    557         var actualArray = actual.split(' ');
    558         for (var i = 0; i < actualArray.length; i++) {
    559             var actualValue = actualArray[i];
    560             if (expectedArray.indexOf(actualValue) == -1 &&
    561                 (expectedArray.indexOf('FAIL') == -1 ||
    562                  (actualValue != 'TEXT' && actualValue != 'IMAGE+TEXT' && actualValue != 'AUDIO')))
    563                 isExpected = false;
    564         }
    565     }
    566     return isExpected;
    567 }
    568 
    569 function processGlobalStateFor(testObject)
    570 {
    571     var test = testObject.name;
    572     if (testObject.has_stderr)
    573         globalState().testsWithStderr.push(testObject);
    574 
    575     globalState().hasHttpTests = globalState().hasHttpTests || test.indexOf('http/') == 0;
    576 
    577     var actual = testObject.actual;   
    578     var expected = testObject.expected || 'PASS';
    579     if (globalState().results.uses_expectations_file)
    580         testObject.isExpected = isFailureExpected(expected, actual);
    581 
    582     if (actual == 'MISSING') {
    583         // FIXME: make sure that new-run-webkit-tests spits out an -actual.txt file for
    584         // tests with MISSING results.
    585         globalState().missingResults.push(testObject);
    586         return;
    587     }
    588 
    589     var actualTokens = actual.split(' ');
    590     var passedWithImageOnlyFailureInRetry = actualTokens[0] == 'TEXT' && actualTokens[1] == 'IMAGE';
    591     if (actualTokens[1] && actual.indexOf('PASS') != -1 || (!globalState().results.pixel_tests_enabled && passedWithImageOnlyFailureInRetry)) {
    592         globalState().flakyPassTests.push(testObject);
    593         return;
    594     }
    595 
    596     if (actual == 'PASS' && expected != 'PASS') {
    597         if (expected != 'IMAGE' || (globalState().results.pixel_tests_enabled || testObject.reftest_type)) {
    598             globalState().unexpectedPassTests.push(testObject);
    599         }
    600         return;
    601     }
    602 
    603     if (actual == 'CRASH') {
    604         globalState().crashTests.push(testObject);
    605         return;
    606     }
    607 
    608     if (actual == 'TIMEOUT') {
    609         globalState().timeoutTests.push(testObject);
    610         return;
    611     }
    612    
    613     globalState().failingTests.push(testObject);
    614 }
    615 
    616 function toggleImages()
    617 {
    618     var images = document.querySelectorAll('.animatedImage');
    619     var imageTexts = document.querySelectorAll('.imageText');
    620     for (var i = 0, len = images.length; i < len; i++) {
    621         var image = images[i];
    622         var text = imageTexts[i];
    623         if (text.textContent == 'Expected Image') {
    624             text.textContent = 'Actual Image';
    625             image.src = image.getAttribute('data-prefix') + '-actual.png';
     1678    constructor() {
     1679        this.currentTestIndex = -1;
     1680        this.flaggedTests = {};
     1681        document.addEventListener('keypress', this, false);
     1682    }
     1683   
     1684    handleEvent(event)
     1685    {
     1686        if (event.type == 'keypress') {
     1687            this.handleKeyEvent(event);
     1688            return;
     1689        }
     1690    }
     1691
     1692    handleKeyEvent(event)
     1693    {
     1694        if (event.metaKey || event.shiftKey || event.ctrlKey)
     1695            return;
     1696
     1697        switch (String.fromCharCode(event.charCode)) {
     1698            case 'i':
     1699                this._scrollToFirstTest();
     1700                break;
     1701            case 'j':
     1702                this._scrollToNextTest();
     1703                break;
     1704            case 'k':
     1705                this._scrollToPreviousTest();
     1706                break;
     1707            case 'l':
     1708                this._scrollToLastTest();
     1709                break;
     1710            case 'e':
     1711                this._expandCurrentTest();
     1712                break;
     1713            case 'c':
     1714                this._collapseCurrentTest();
     1715                break;
     1716            case 't':
     1717                this._toggleCurrentTest();
     1718                break;
     1719            case 'f':
     1720                this._toggleCurrentTestFlagged();
     1721                break;
     1722        }
     1723    }
     1724
     1725    _scrollToFirstTest()
     1726    {
     1727        if (this._setCurrentTest(0))
     1728            this._scrollToCurrentTest();
     1729    }
     1730
     1731    _scrollToLastTest()
     1732    {
     1733        let links = controller.visibleTests();
     1734        if (this._setCurrentTest(links.length - 1))
     1735            this._scrollToCurrentTest();
     1736    }
     1737
     1738    _scrollToNextTest()
     1739    {
     1740        if (this.currentTestIndex == -1)
     1741            this._scrollToFirstTest();
     1742        else if (this._setCurrentTest(this.currentTestIndex + 1))
     1743            this._scrollToCurrentTest();
     1744    }
     1745
     1746    _scrollToPreviousTest()
     1747    {
     1748        if (this.currentTestIndex == -1)
     1749            this._scrollToLastTest();
     1750        else if (this._setCurrentTest(this.currentTestIndex - 1))
     1751            this._scrollToCurrentTest();
     1752    }
     1753
     1754    _currentTestLink()
     1755    {
     1756        let links = controller.visibleTests();
     1757        return links[this.currentTestIndex];
     1758    }
     1759
     1760    _currentTestExpandLink()
     1761    {
     1762        return this._currentTestLink().querySelector('.expand-button-text');
     1763    }
     1764
     1765    _expandCurrentTest()
     1766    {
     1767        controller.expandExpectations(this._currentTestExpandLink());
     1768    }
     1769
     1770    _collapseCurrentTest()
     1771    {
     1772        controller.collapseExpectations(this._currentTestExpandLink());
     1773    }
     1774
     1775    _toggleCurrentTest()
     1776    {
     1777        controller.toggleExpectations(this._currentTestExpandLink());
     1778    }
     1779
     1780    _toggleCurrentTestFlagged()
     1781    {
     1782        let testLink = this._currentTestLink();
     1783        this.flagTest(testLink, !testLink.classList.contains('flagged'));
     1784    }
     1785
     1786    // FIXME: Test navigator shouldn't know anything about flagging. It should probably call out to TestFlagger or something.
     1787    // FIXME: Batch flagging (avoid updateFlaggedTests on each test).
     1788    flagTest(testTbody, shouldFlag)
     1789    {
     1790        let testName = testTbody.querySelector('.test-link').innerText;
     1791   
     1792        if (shouldFlag) {
     1793            testTbody.classList.add('flagged');
     1794            this.flaggedTests[testName] = 1;
    6261795        } else {
    627             text.textContent = 'Expected Image';
    628             image.src = image.getAttribute('data-prefix') + '-expected.png';
    629         }
    630     }
    631 }
    632 
    633 function textResultLinks(prefix)
    634 {
    635     var html = resultLink(prefix, '-expected.txt', 'expected') +
    636         resultLink(prefix, '-actual.txt', 'actual') +
    637         resultLink(prefix, '-diff.txt', 'diff');
    638 
    639     if (globalState().results.has_pretty_patch)
    640         html += resultLink(prefix, '-pretty-diff.html', 'pretty diff');
    641 
    642     if (globalState().results.has_wdiff)
    643         html += resultLink(prefix, '-wdiff.html', 'wdiff');
    644 
    645     return html;
    646 }
    647 
    648 function imageResultsCell(testObject, testPrefix, actual) {
    649     var row = '';
    650 
    651     if (actual.indexOf('IMAGE') != -1) {
    652         var testExtension = splitExtension(testObject.name)[1];
    653         globalState().hasImageFailures = true;
    654 
    655         if (testObject.reftest_type && testObject.reftest_type.indexOf('!=') != -1) {
    656             row += resultLink(layoutTestsBasePath() + testPrefix, '-expected-mismatch.' + testExtension, 'ref mismatch');
    657             row += resultLink(testPrefix, '-actual.png', 'actual');
    658         } else {
    659             if (testObject.reftest_type && testObject.reftest_type.indexOf('==') != -1) {
    660                 row += resultLink(layoutTestsBasePath() + testPrefix, '-expected.' + testExtension, 'reference');
    661             }
    662             if (globalState().shouldToggleImages) {
    663                 row += resultLink(testPrefix, '-diffs.html', 'images');
    664             } else {
    665                 row += resultLink(testPrefix, '-expected.png', 'expected');
    666                 row += resultLink(testPrefix, '-actual.png', 'actual');
    667             }
    668 
    669             var diff = testObject.image_diff_percent;
    670             row += resultLink(testPrefix, '-diff.png', 'diff (' + diff + '%)');
    671         }
    672     }
    673 
    674     if (actual.indexOf('MISSING') != -1 && testObject.is_missing_image)
    675         row += resultLink(testPrefix, '-actual.png', 'png result');
    676 
    677     return row;
    678 }
    679 
    680 function flakinessDashboardURLForTests(testObjects)
    681 {
    682     var testList = "";
    683     for (var i = 0; i < testObjects.length; ++i) {
    684         testList += testObjects[i].name;
    685 
    686         if (i != testObjects.length - 1)
    687             testList += ",";
    688     }
    689 
    690     return 'http://webkit-test-results.webkit.org/dashboards/flakiness_dashboard.html#showAllRuns=true&tests=' + encodeURIComponent(testList);
    691 }
    692 
    693 function tableRow(testObject)
    694 {   
    695     var row = '<tbody'
    696     if (globalState().results.uses_expectations_file)
    697         row += ' class="' + (testObject.isExpected ? 'expected' : '') + '"';
    698     if (testObject.reftest_type && testObject.reftest_type.indexOf('!=') != -1)
    699         row += ' mismatchreftest=true';
    700     row += '><tr>';
    701 
    702     row += '<td>' + testLinkWithExpandButton(testObject.name) + '</td>';
    703 
    704     var testPrefix = stripExtension(testObject.name);
    705     row += '<td>';
    706    
    707     var actual = testObject.actual;
    708     if (actual.indexOf('TEXT') != -1) {
    709         globalState().hasTextFailures = true;
    710         row += textResultLinks(testPrefix);
    711     }
    712    
    713     if (actual.indexOf('AUDIO') != -1) {
    714         row += resultLink(testPrefix, '-expected.wav', 'expected audio');
    715         row += resultLink(testPrefix, '-actual.wav', 'actual audio');
    716         row += resultLink(testPrefix, '-diff.txt', 'textual diff');
    717     }
    718 
    719     if (actual.indexOf('MISSING') != -1) {
    720         if (testObject.is_missing_audio)
    721             row += resultLink(testPrefix, '-actual.wav', 'audio result');
    722         if (testObject.is_missing_text)
    723             row += resultLink(testPrefix, '-actual.txt', 'result');
    724     }
    725 
    726     var actualTokens = actual.split(/\s+/);
    727     var cell = imageResultsCell(testObject, testPrefix, actualTokens[0]);
    728     if (!cell && actualTokens.length > 1)
    729         cell = imageResultsCell(testObject, 'retries/' + testPrefix, actualTokens[1]);
    730 
    731     row += '</td><td>' + cell + '</td>';
    732 
    733     if (globalState().results.uses_expectations_file || actual.indexOf(' ') != -1)
    734         row += '<td>' + actual + '</td>';
    735 
    736     if (globalState().results.uses_expectations_file)
    737         row += '<td>' + (actual.indexOf('MISSING') == -1 ? testObject.expected : '') + '</td>';
    738 
    739     row += '<td><a href="' + flakinessDashboardURLForTests([testObject]) + '">history</a></td>';
    740 
    741     row += '</tr></tbody>';
    742     return row;
    743 }
    744 
    745 function forEachTest(handler, opt_tree, opt_prefix)
    746 {
    747     var tree = opt_tree || globalState().results.tests;
    748     var prefix = opt_prefix || '';
    749 
    750     for (var key in tree) {
    751         var newPrefix = prefix ? (prefix + '/' + key) : key;
    752         if ('actual' in tree[key]) {
    753             var testObject = tree[key];
    754             testObject.name = newPrefix;
    755             handler(testObject);
    756         } else
    757             forEachTest(handler, tree[key], newPrefix);
    758     }
    759 }
    760 
    761 function forOtherCrashes()
    762 {
    763     var tree = globalState().results.other_crashes;
    764     for (var key in tree) {
    765             var testObject = tree[key];
    766             testObject.name = key;
    767             globalState().crashOther.push(testObject);
    768     }
    769 }
    770 
    771 function hasUnexpected(tests)
    772 {
    773     return tests.some(function (test) { return !test.isExpected; });
    774 }
    775 
    776 function updateTestlistCounts()
    777 {
    778     forEach(document.querySelectorAll('.test-list-count'), function(count) {
    779         var container = parentOfType(count, 'div');
    780         var testContainers;
    781         if (onlyShowUnexpectedFailures())
    782             testContainers = container.querySelectorAll('tbody:not(.expected)');
    783         else
    784             testContainers = container.querySelectorAll('tbody');
    785 
    786         count.textContent = testContainers.length;
    787     })
    788 }
    789 
    790 function flagAll(headerLink)
    791 {
    792     var tests = visibleTests(parentOfType(headerLink, 'div'));
    793     forEach(tests, function(tests) {
    794         var shouldFlag = true;
    795         TestNavigator.flagTest(tests, shouldFlag);
    796     })
    797 }
    798 
    799 function testListHeaderHtml(header)
    800 {
    801     return '<h1>' + header + ' (<span class=test-list-count></span>): <a href="#" class=flag-all onclick="flagAll(this)">flag all</a></h1>';
    802 }
    803 
    804 function testList(tests, header, tableId)
    805 {
    806     tests.sort(function (a, b) { return a.name.localeCompare(b.name) });
    807 
    808     var html = '<div' + ((!hasUnexpected(tests) && tableId != 'stderr-table') ? ' class=expected' : '') + ' id=' + tableId + '>' +
    809         testListHeaderHtml(header) + '<table>';
    810 
    811     // FIXME: add the expected failure column for all the test lists if globalState().results.uses_expectations_file
    812     if (tableId == 'passes-table')
    813         html += '<thead><th>test</th><th>expected failure</th></thead>';
    814 
    815     for (var i = 0; i < tests.length; i++) {
    816         var testObject = tests[i];
    817         var test = testObject.name;
    818         html += '<tbody';
    819         if (globalState().results.uses_expectations_file)
    820             html += ' class="' + ((testObject.isExpected && tableId != 'stderr-table') ? 'expected' : '') + '"';
    821         html += '><tr><td>';
    822         if (tableId == 'passes-table')
    823             html += testLink(test);
    824         else if (tableId == 'other-crash-tests-table')
    825             html += testWithExpandButton(test);
    826         else
    827             html += testLinkWithExpandButton(test);
    828 
    829         html += '</td><td>';
    830 
    831         if (tableId == 'stderr-table')
    832             html += resultLink(stripExtension(test), '-stderr.txt', 'stderr');
    833         else if (tableId == 'passes-table')
    834             html += testObject.expected;
    835         else if (tableId == 'other-crash-tests-table')
    836             html += resultLink(stripExtension(test), '-crash-log.txt', 'crash log');
    837         else if (tableId == 'crash-tests-table') {
    838             html += resultLink(stripExtension(test), '-crash-log.txt', 'crash log');
    839             html += resultLink(stripExtension(test), '-sample.txt', 'sample');
    840         } else if (tableId == 'timeout-tests-table') {
    841             // FIXME: only include timeout actual/diff results here if we actually spit out results for timeout tests.
    842             html += textResultLinks(stripExtension(test));
    843         }
    844 
    845         if (tableId != 'other-crash-tests-table')
    846             html += '</td><td><a href="' + flakinessDashboardURLForTests([testObject]) + '">history</a></td>';
    847 
    848         html += '</tr></tbody>';
    849     }
    850     html += '</table></div>';
    851     return html;
    852 }
    853 
    854 function toArray(nodeList)
    855 {
    856     return Array.prototype.slice.call(nodeList);
    857 }
    858 
    859 function trim(string)
    860 {
    861     return string.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
    862 }
    863 
    864 // Just a namespace for code management.
    865 var TableSorter = {};
    866 
    867 TableSorter._forwardArrow = '<svg style="width:10px;height:10px"><polygon points="0,0 10,0 5,10" style="fill:#ccc"></svg>';
    868 
    869 TableSorter._backwardArrow = '<svg style="width:10px;height:10px"><polygon points="0,10 10,10 5,0" style="fill:#ccc"></svg>';
    870 
    871 TableSorter._sortedContents = function(header, arrow)
    872 {
    873     return arrow + ' ' + trim(header.textContent) + ' ' + arrow;
    874 }
    875 
    876 TableSorter._updateHeaderClassNames = function(newHeader)
    877 {
    878     var sortHeader = document.querySelector('.sortHeader');
    879     if (sortHeader) {
    880         if (sortHeader == newHeader) {
    881             var isAlreadyReversed = sortHeader.classList.contains('reversed');
    882             if (isAlreadyReversed)
    883                 sortHeader.classList.remove('reversed');
    884             else
    885                 sortHeader.classList.add('reversed');
    886         } else {
    887             sortHeader.textContent = sortHeader.textContent;
    888             sortHeader.classList.remove('sortHeader');
    889             sortHeader.classList.remove('reversed');
    890         }
    891     }
    892 
    893     newHeader.classList.add('sortHeader');
    894 }
    895 
    896 TableSorter._textContent = function(tbodyRow, column)
    897 {
    898     return tbodyRow.querySelectorAll('td')[column].textContent;
    899 }
    900 
    901 TableSorter._sortRows = function(newHeader, reversed)
    902 {
    903     var testsTable = document.getElementById('results-table');
    904     var headers = toArray(testsTable.querySelectorAll('th'));
    905     var sortColumn = headers.indexOf(newHeader);
    906 
    907     var rows = toArray(testsTable.querySelectorAll('tbody'));
    908 
    909     rows.sort(function(a, b) {
    910         // Only need to support lexicographic sort for now.
    911         var aText = TableSorter._textContent(a, sortColumn);
    912         var bText = TableSorter._textContent(b, sortColumn);
    913        
    914         // Forward sort equal values by test name.
    915         if (sortColumn && aText == bText) {
    916             var aTestName = TableSorter._textContent(a, 0);
    917             var bTestName = TableSorter._textContent(b, 0);
    918             if (aTestName == bTestName)
    919                 return 0;
    920             return aTestName < bTestName ? -1 : 1;
    921         }
    922 
    923         if (reversed)
    924             return aText < bText ? 1 : -1;
    925         else
    926             return aText < bText ? -1 : 1;
    927     });
    928 
    929     for (var i = 0; i < rows.length; i++)
    930         testsTable.appendChild(rows[i]);
    931 }
    932 
    933 TableSorter.sortColumn = function(columnNumber)
    934 {
    935     var newHeader = document.getElementById('results-table').querySelectorAll('th')[columnNumber];
    936     TableSorter._sort(newHeader);
    937 }
    938 
    939 TableSorter.handleClick = function(e)
    940 {
    941     var newHeader = e.target;
    942     if (newHeader.localName != 'th')
    943         return;
    944     TableSorter._sort(newHeader);
    945 }
    946 
    947 TableSorter._sort = function(newHeader)
    948 {
    949     TableSorter._updateHeaderClassNames(newHeader);
    950    
    951     var reversed = newHeader.classList.contains('reversed');
    952     var sortArrow = reversed ? TableSorter._backwardArrow : TableSorter._forwardArrow;
    953     newHeader.innerHTML = TableSorter._sortedContents(newHeader, sortArrow);
    954    
    955     TableSorter._sortRows(newHeader, reversed);
    956 }
    957 
    958 var PixelZoomer = {};
    959 
    960 PixelZoomer.showOnDelay = true;
    961 PixelZoomer._zoomFactor = 6;
    962 
    963 var kResultWidth = 800;
    964 var kResultHeight = 600;
    965 
    966 var kZoomedResultWidth = kResultWidth * PixelZoomer._zoomFactor;
    967 var kZoomedResultHeight = kResultHeight * PixelZoomer._zoomFactor;
    968 
    969 PixelZoomer._zoomImageContainer = function(url)
    970 {
    971     var container = document.createElement('div');
    972     container.className = 'zoom-image-container';
    973 
    974     var title = url.match(/\-([^\-]*)\.png/)[1];
    975    
    976     var label = document.createElement('div');
    977     label.className = 'label';
    978     label.appendChild(document.createTextNode(title));
    979     container.appendChild(label);
    980    
    981     var imageContainer = document.createElement('div');
    982     imageContainer.className = 'scaled-image-container';
    983    
    984     var image = new Image();
    985     image.src = url;
    986     image.style.width = kZoomedResultWidth + 'px';
    987     image.style.height = kZoomedResultHeight + 'px';
    988     image.style.border = '1px solid black';
    989     imageContainer.appendChild(image);
    990     container.appendChild(imageContainer);
    991    
    992     return container;
    993 }
    994 
    995 PixelZoomer._createContainer = function(e)
    996 {
    997     var tbody = parentOfType(e.target, 'tbody');
    998     var row = tbody.querySelector('tr');
    999     var imageDiffLinks = row.querySelectorAll('a[href$=".png"]');
    1000    
    1001     var container = document.createElement('div');
    1002     container.className = 'pixel-zoom-container';
    1003    
    1004     var html = '';
    1005    
    1006     var togglingImageLink = row.querySelector('a[href$="-diffs.html"]');
    1007     if (togglingImageLink) {
    1008         var prefix = togglingImageLink.getAttribute('data-prefix');
    1009         container.appendChild(PixelZoomer._zoomImageContainer(prefix + '-expected.png'));
    1010         container.appendChild(PixelZoomer._zoomImageContainer(prefix + '-actual.png'));
    1011     }
    1012    
    1013     for (var i = 0; i < imageDiffLinks.length; i++)
    1014         container.appendChild(PixelZoomer._zoomImageContainer(imageDiffLinks[i].href));
    1015 
    1016     document.body.appendChild(container);
    1017     PixelZoomer._drawAll();
    1018 }
    1019 
    1020 PixelZoomer._draw = function(imageContainer)
    1021 {
    1022     var image = imageContainer.querySelector('img');
    1023     var containerBounds = imageContainer.getBoundingClientRect();
    1024     image.style.left = (containerBounds.width / 2 - PixelZoomer._percentX * kZoomedResultWidth) + 'px';
    1025     image.style.top = (containerBounds.height / 2 - PixelZoomer._percentY * kZoomedResultHeight) + 'px';
    1026 }
    1027 
    1028 PixelZoomer._drawAll = function()
    1029 {
    1030     forEach(document.querySelectorAll('.pixel-zoom-container .scaled-image-container'), PixelZoomer._draw);
    1031 }
    1032 
    1033 PixelZoomer.handleMouseOut = function(e)
    1034 {
    1035     if (e.relatedTarget && e.relatedTarget.tagName != 'IFRAME')
    1036         return;
    1037 
    1038     // If e.relatedTarget is null, we've moused out of the document.
    1039     var container = document.querySelector('.pixel-zoom-container');
    1040     if (container)
    1041         remove(container);
    1042 }
    1043 
    1044 PixelZoomer.handleMouseMove = function(e) {
    1045     if (PixelZoomer._mouseMoveTimeout)
    1046         clearTimeout(PixelZoomer._mouseMoveTimeout);
    1047 
    1048     if (parentOfType(e.target, '.pixel-zoom-container'))
    1049         return;
    1050 
    1051     var container = document.querySelector('.pixel-zoom-container');
    1052    
    1053     var resultContainer = (e.target.className == 'result-container') ?
    1054         e.target : parentOfType(e.target, '.result-container');
    1055     if (!resultContainer || !resultContainer.querySelector('img')) {
    1056         if (container)
    1057             remove(container);
    1058         return;
    1059     }
    1060 
    1061     var targetLocation = e.target.getBoundingClientRect();
    1062     PixelZoomer._percentX = (e.clientX - targetLocation.left) / targetLocation.width;
    1063     PixelZoomer._percentY = (e.clientY - targetLocation.top) / targetLocation.height;
    1064 
    1065     if (!container) {
    1066         if (PixelZoomer.showOnDelay) {
    1067             PixelZoomer._mouseMoveTimeout = setTimeout(function() {
    1068                 PixelZoomer._createContainer(e);
    1069             }, 400);
    1070             return;
    1071         }
    1072 
    1073         PixelZoomer._createContainer(e);
    1074         return;
    1075     }
    1076    
    1077     PixelZoomer._drawAll();
    1078 }
    1079 
    1080 document.addEventListener('mousemove', PixelZoomer.handleMouseMove, false);
    1081 document.addEventListener('mouseout', PixelZoomer.handleMouseOut, false);
    1082 
    1083 var TestNavigator = {};
    1084 
    1085 TestNavigator.reset = function() {
    1086     TestNavigator.currentTestIndex = -1;
    1087     TestNavigator.flaggedTests = {};
    1088 }
    1089 
    1090 TestNavigator.handleKeyEvent = function(event)
    1091 {
    1092     if (event.metaKey || event.shiftKey || event.ctrlKey)
    1093         return;
    1094 
    1095     switch (String.fromCharCode(event.charCode)) {
    1096         case 'i':
    1097             TestNavigator._scrollToFirstTest();
    1098             break;
    1099         case 'j':
    1100             TestNavigator._scrollToNextTest();
    1101             break;
    1102         case 'k':
    1103             TestNavigator._scrollToPreviousTest();
    1104             break;
    1105         case 'l':
    1106             TestNavigator._scrollToLastTest();
    1107             break;
    1108         case 'e':
    1109             TestNavigator._expandCurrentTest();
    1110             break;
    1111         case 'c':
    1112             TestNavigator._collapseCurrentTest();
    1113             break;
    1114         case 't':
    1115             TestNavigator._toggleCurrentTest();
    1116             break;
    1117         case 'f':
    1118             TestNavigator._toggleCurrentTestFlagged();
    1119             break;
    1120     }
    1121 }
    1122 
    1123 TestNavigator._scrollToFirstTest = function()
    1124 {
    1125     if (TestNavigator._setCurrentTest(0))
    1126         TestNavigator._scrollToCurrentTest();
    1127 }
    1128 
    1129 TestNavigator._scrollToLastTest = function()
    1130 {
    1131     var links = visibleTests();
    1132     if (TestNavigator._setCurrentTest(links.length - 1))
    1133         TestNavigator._scrollToCurrentTest();
    1134 }
    1135 
    1136 TestNavigator._scrollToNextTest = function()
    1137 {
    1138     if (TestNavigator.currentTestIndex == -1)
    1139         TestNavigator._scrollToFirstTest();
    1140     else if (TestNavigator._setCurrentTest(TestNavigator.currentTestIndex + 1))
    1141         TestNavigator._scrollToCurrentTest();
    1142 }
    1143 
    1144 TestNavigator._scrollToPreviousTest = function()
    1145 {
    1146     if (TestNavigator.currentTestIndex == -1)
    1147         TestNavigator._scrollToLastTest();
    1148     else if (TestNavigator._setCurrentTest(TestNavigator.currentTestIndex - 1))
    1149         TestNavigator._scrollToCurrentTest();
    1150 }
    1151 
    1152 TestNavigator._currentTestLink = function()
    1153 {
    1154     var links = visibleTests();
    1155     return links[TestNavigator.currentTestIndex];
    1156 }
    1157 
    1158 TestNavigator._currentTestExpandLink = function()
    1159 {
    1160     return TestNavigator._currentTestLink().querySelector('.expand-button-text');
    1161 }
    1162 
    1163 TestNavigator._expandCurrentTest = function()
    1164 {
    1165     expandExpectations(TestNavigator._currentTestExpandLink());
    1166 }
    1167 
    1168 TestNavigator._collapseCurrentTest = function()
    1169 {
    1170     collapseExpectations(TestNavigator._currentTestExpandLink());
    1171 }
    1172 
    1173 TestNavigator._toggleCurrentTest = function()
    1174 {
    1175     toggleExpectations(TestNavigator._currentTestExpandLink());
    1176 }
    1177 
    1178 TestNavigator._toggleCurrentTestFlagged = function()
    1179 {
    1180     var testLink = TestNavigator._currentTestLink();
    1181     TestNavigator.flagTest(testLink, !testLink.classList.contains('flagged'));
    1182 }
    1183 
    1184 // FIXME: Test navigator shouldn't know anything about flagging. It should probably call out to TestFlagger or something.
    1185 TestNavigator.flagTest = function(testTbody, shouldFlag)
    1186 {
    1187     var testName = testTbody.querySelector('.test-link').innerText;
    1188    
    1189     if (shouldFlag) {
    1190         testTbody.classList.add('flagged');
    1191         TestNavigator.flaggedTests[testName] = 1;
    1192     } else {
    1193         testTbody.classList.remove('flagged');
    1194         delete TestNavigator.flaggedTests[testName];
    1195     }
    1196 
    1197     TestNavigator.updateFlaggedTests();
    1198 }
    1199 
    1200 TestNavigator.updateFlaggedTests = function()
    1201 {
    1202     var flaggedTestTextbox = document.getElementById('flagged-tests');
    1203     if (!flaggedTestTextbox) {
    1204         var flaggedTestContainer = document.createElement('div');
    1205         flaggedTestContainer.id = 'flagged-test-container';
    1206         flaggedTestContainer.className = 'floating-panel';
    1207         flaggedTestContainer.innerHTML = '<h2>Flagged Tests</h2><pre id="flagged-tests" contentEditable></pre>';
    1208         document.body.appendChild(flaggedTestContainer);
    1209 
    1210         flaggedTestTextbox = document.getElementById('flagged-tests');
    1211     }
    1212 
    1213     var flaggedTests = Object.keys(this.flaggedTests);
    1214     flaggedTests.sort();
    1215     var separator = document.getElementById('use-newlines').checked ? '\n' : ' ';
    1216     flaggedTestTextbox.innerHTML = flaggedTests.join(separator);
    1217     document.getElementById('flagged-test-container').style.display = flaggedTests.length ? '' : 'none';
    1218 }
    1219 
    1220 TestNavigator._setCurrentTest = function(testIndex)
    1221 {
    1222     var links = visibleTests();
    1223     if (testIndex < 0 || testIndex >= links.length)
    1224         return false;
    1225 
    1226     var currentTest = links[TestNavigator.currentTestIndex];
    1227     if (currentTest)
    1228         currentTest.classList.remove('current');
    1229 
    1230     TestNavigator.currentTestIndex = testIndex;
    1231 
    1232     currentTest = links[TestNavigator.currentTestIndex];
    1233     currentTest.classList.add('current');
    1234 
    1235     return true;
    1236 }
    1237 
    1238 TestNavigator._scrollToCurrentTest = function()
    1239 {
    1240     var targetLink = TestNavigator._currentTestLink();
    1241     if (!targetLink)
    1242         return;
    1243 
    1244     var rowRect = targetLink.getBoundingClientRect();
    1245     // rowRect is in client coords (i.e. relative to viewport), so we just want to add its top to the current scroll position.
    1246     document.body.scrollTop += rowRect.top;
    1247 }
    1248 
    1249 TestNavigator.onlyShowUnexpectedFailuresChanged = function()
    1250 {
    1251     var currentTest = document.querySelector('.current');
    1252     if (!currentTest)
    1253         return;
    1254 
    1255     // If our currentTest became hidden, reset the currentTestIndex.
    1256     if (onlyShowUnexpectedFailures() && currentTest.classList.contains('expected'))
    1257         TestNavigator._scrollToFirstTest();
    1258     else {
    1259         // Recompute TestNavigator.currentTestIndex
    1260         var links = visibleTests();
    1261         TestNavigator.currentTestIndex = links.indexOf(currentTest);
    1262     }
    1263 }
    1264 
    1265 document.addEventListener('keypress', TestNavigator.handleKeyEvent, false);
    1266 
    1267 
    1268 function onlyShowUnexpectedFailures()
    1269 {
    1270     return document.getElementById('unexpected-results').checked;
    1271 }
    1272 
    1273 function handleUnexpectedResultsChange()
    1274 {
    1275     OptionWriter.save();
    1276     updateExpectedFailures();
    1277 }
    1278 
    1279 function updateExpectedFailures()
    1280 {
    1281     document.getElementById('unexpected-style').textContent = onlyShowUnexpectedFailures() ?
    1282         '.expected { display: none; }' : '';
    1283 
    1284     updateTestlistCounts();
    1285     TestNavigator.onlyShowUnexpectedFailuresChanged();
    1286 }
    1287 
    1288 var OptionWriter = {};
    1289 
    1290 OptionWriter._key = 'run-webkit-tests-options';
    1291 
    1292 OptionWriter.save = function()
    1293 {
    1294     var options = document.querySelectorAll('label input');
    1295     var data = {};
    1296     for (var i = 0, len = options.length; i < len; i++) {
    1297         var option = options[i];
    1298         data[option.id] = option.checked;
    1299     }
    1300     try {
    1301         localStorage.setItem(OptionWriter._key, JSON.stringify(data));
    1302     } catch (err) {
    1303         if (err.name != "SecurityError")
    1304             throw err;
    1305     }
    1306 }
    1307 
    1308 OptionWriter.apply = function()
    1309 {
    1310     var json;
    1311     try {
    1312         json = localStorage.getItem(OptionWriter._key);
    1313     } catch (err) {
    1314        if (err.name != "SecurityError")
    1315           throw err;
    1316     }
    1317 
    1318     if (!json) {
    1319         updateAllOptions();
    1320         return;
    1321     }
    1322 
    1323     var data = JSON.parse(json);
    1324     for (var id in data) {
    1325         var input = document.getElementById(id);
    1326         if (input)
    1327             input.checked = data[id];
    1328     }
    1329     updateAllOptions();
    1330 }
    1331 
    1332 function updateAllOptions()
    1333 {
    1334     forEach(document.querySelectorAll('#options-menu input'), function(input) { input.onchange(); });
    1335 }
    1336 
    1337 function handleToggleUseNewlines()
    1338 {
    1339     OptionWriter.save();
    1340     TestNavigator.updateFlaggedTests();
    1341 }
    1342 
    1343 function handleToggleImagesChange()
    1344 {
    1345     OptionWriter.save();
    1346     updateTogglingImages();
    1347 }
    1348 
    1349 function updateTogglingImages()
    1350 {
    1351     var shouldToggle = document.getElementById('toggle-images').checked;
    1352     globalState().shouldToggleImages = shouldToggle;
    1353    
    1354     if (shouldToggle) {
    1355         forEach(document.querySelectorAll('table:not(#missing-table) tbody:not([mismatchreftest]) a[href$=".png"]'), convertToTogglingHandler(function(prefix) {
    1356             return resultLink(prefix, '-diffs.html', 'images');
    1357         }));
    1358         forEach(document.querySelectorAll('table:not(#missing-table) tbody:not([mismatchreftest]) img[src$=".png"]'), convertToTogglingHandler(togglingImage));
    1359     } else {
    1360         forEach(document.querySelectorAll('a[href$="-diffs.html"]'), convertToNonTogglingHandler(resultLink));
    1361         forEach(document.querySelectorAll('.animatedImage'), convertToNonTogglingHandler(function (absolutePrefix, suffix) {
    1362             return resultIframe(absolutePrefix + suffix);
    1363         }));
    1364     }
    1365 
    1366     updateImageTogglingTimer();
    1367 }
    1368 
    1369 function getResultContainer(node)
    1370 {
    1371     return (node.tagName == 'IMG') ? parentOfType(node, '.result-container') : node;
    1372 }
    1373 
    1374 function convertToTogglingHandler(togglingImageFunction)
    1375 {
    1376     return function(node) {
    1377         var url = (node.tagName == 'IMG') ? node.src : node.href;
    1378         if (url.match('-expected.png$'))
    1379             remove(getResultContainer(node));
    1380         else if (url.match('-actual.png$')) {
    1381             var name = parentOfType(node, 'tbody').querySelector('.test-link').textContent;
    1382             getResultContainer(node).outerHTML = togglingImageFunction(stripExtension(name));
    1383         }
    1384     }
    1385 }
    1386 
    1387 function convertToNonTogglingHandler(resultFunction)
    1388 {
    1389     return function(node) {
    1390         var prefix = node.getAttribute('data-prefix');
    1391         getResultContainer(node).outerHTML = resultFunction(prefix, '-expected.png', 'expected') + resultFunction(prefix, '-actual.png', 'actual');
    1392     }
    1393 }
    1394 
    1395 function toggleOptionsMenu()
    1396 {
    1397     var menu = document.getElementById('options-menu');
    1398     menu.className = (menu.className == 'hidden-menu') ? '' : 'hidden-menu';
    1399 }
     1796            testTbody.classList.remove('flagged');
     1797            delete this.flaggedTests[testName];
     1798        }
     1799
     1800        this.updateFlaggedTests();
     1801    }
     1802
     1803    updateFlaggedTests()
     1804    {
     1805        let flaggedTestTextbox = document.getElementById('flagged-tests');
     1806        if (!flaggedTestTextbox) {
     1807            let flaggedTestContainer = document.createElement('div');
     1808            flaggedTestContainer.id = 'flagged-test-container';
     1809            flaggedTestContainer.className = 'floating-panel';
     1810            flaggedTestContainer.innerHTML = '<h2>Flagged Tests</h2><pre id="flagged-tests" contentEditable></pre>';
     1811            document.body.appendChild(flaggedTestContainer);
     1812
     1813            flaggedTestTextbox = document.getElementById('flagged-tests');
     1814        }
     1815
     1816        let flaggedTests = Object.keys(this.flaggedTests);
     1817        flaggedTests.sort();
     1818        let separator = document.getElementById('use-newlines').checked ? '\n' : ' ';
     1819        flaggedTestTextbox.innerHTML = flaggedTests.join(separator);
     1820        document.getElementById('flagged-test-container').style.display = flaggedTests.length ? '' : 'none';
     1821    }
     1822
     1823    _setCurrentTest(testIndex)
     1824    {
     1825        let links = controller.visibleTests();
     1826        if (testIndex < 0 || testIndex >= links.length)
     1827            return false;
     1828
     1829        let currentTest = links[this.currentTestIndex];
     1830        if (currentTest)
     1831            currentTest.classList.remove('current');
     1832
     1833        this.currentTestIndex = testIndex;
     1834
     1835        currentTest = links[this.currentTestIndex];
     1836        currentTest.classList.add('current');
     1837
     1838        return true;
     1839    }
     1840
     1841    _scrollToCurrentTest()
     1842    {
     1843        let targetLink = this._currentTestLink();
     1844        if (!targetLink)
     1845            return;
     1846
     1847        let rowRect = targetLink.getBoundingClientRect();
     1848        // rowRect is in client coords (i.e. relative to viewport), so we just want to add its top to the current scroll position.
     1849        document.body.scrollTop += rowRect.top;
     1850    }
     1851
     1852    onlyShowUnexpectedFailuresChanged()
     1853    {
     1854        let currentTest = document.querySelector('.current');
     1855        if (!currentTest)
     1856            return;
     1857
     1858        // If our currentTest became hidden, reset the currentTestIndex.
     1859        if (controller.onlyShowUnexpectedFailures() && currentTest.classList.contains('expected'))
     1860            this._scrollToFirstTest();
     1861        else {
     1862            // Recompute this.currentTestIndex
     1863            let links = controller.visibleTests();
     1864            this.currentTestIndex = links.indexOf(currentTest);
     1865        }
     1866    }
     1867};
    14001868
    14011869function handleMouseDown(e)
    14021870{
    1403     if (!parentOfType(e.target, '#options-menu') && e.target.id != 'options-link')
     1871    if (!Utils.parentOfType(e.target, '#options-menu') && e.target.id != 'options-link')
    14041872        document.getElementById('options-menu').className = 'hidden-menu';
    14051873}
     
    14071875document.addEventListener('mousedown', handleMouseDown, false);
    14081876
    1409 function failingTestsTable(tests, title, id)
    1410 {
    1411     if (!tests.length)
    1412         return '';
    1413 
    1414     var numberofUnexpectedFailures = 0;
    1415     var tableRowHtml = '';
    1416     for (var i = 0; i < tests.length; i++){
    1417         tableRowHtml += tableRow(tests[i]);
    1418         if (!tests[i].isExpected)
    1419             numberofUnexpectedFailures++;
    1420     }
    1421 
    1422     var header = '<div';
    1423     if (!hasUnexpected(tests))
    1424         header += ' class=expected';
    1425 
    1426     header += '>' + testListHeaderHtml(title) +
    1427         '<table id="' + id + '"><thead><tr>' +
    1428         '<th>test</th>' +
    1429         '<th id="text-results-header">results</th>' +
    1430         '<th id="image-results-header">image results</th>';
    1431 
    1432     if (globalState().results.uses_expectations_file)
    1433         header += '<th>actual failure</th><th>expected failure</th>';
    1434 
    1435     header += '<th><a href="' + flakinessDashboardURLForTests(tests) + '">history</a></th>';
    1436 
    1437     if (id == 'flaky-tests-table')
    1438         header += '<th>failures</th>';
    1439 
    1440     header += '</tr></thead>';
    1441 
    1442     return header + tableRowHtml + '</table></div>';
    1443 }
    1444 
    1445 function updateTitle()
    1446 {
    1447     var dateString = globalState().results.date;
    1448    
    1449     var title = document.createElement('title');
    1450     title.textContent = 'Layout Test Results from ' + dateString;
    1451     document.head.appendChild(title);
    1452 }
     1877let controller;
     1878let pixelZoomer;
     1879let testNavigator;
    14531880
    14541881function generatePage()
    14551882{
    1456     updateTitle();
    1457     forEachTest(processGlobalStateFor);
    1458     forOtherCrashes();
    1459 
    1460     var html = "";
    1461 
    1462     if (globalState().results.interrupted)
    1463         html += "<p class='stopped-running-early-message'>Testing exited early.</p>"
    1464 
    1465     if (globalState().crashTests.length)
    1466         html += testList(globalState().crashTests, 'Tests that crashed', 'crash-tests-table');
    1467 
    1468     if (globalState().crashOther.length)
    1469         html += testList(globalState().crashOther, 'Other Crashes', 'other-crash-tests-table');
    1470 
    1471     html += failingTestsTable(globalState().failingTests,
    1472         'Tests that failed text/pixel/audio diff', 'results-table');
    1473 
    1474     html += failingTestsTable(globalState().missingResults,
    1475         'Tests that had no expected results (probably new)', 'missing-table');
    1476 
    1477     if (globalState().timeoutTests.length)
    1478         html += testList(globalState().timeoutTests, 'Tests that timed out', 'timeout-tests-table');
    1479 
    1480     if (globalState().testsWithStderr.length)
    1481         html += testList(globalState().testsWithStderr, 'Tests that had stderr output', 'stderr-table');
    1482 
    1483     html += failingTestsTable(globalState().flakyPassTests,
    1484         'Flaky tests (failed the first run and passed on retry)', 'flaky-tests-table');
    1485 
    1486     if (globalState().results.uses_expectations_file && globalState().unexpectedPassTests.length)
    1487         html += testList(globalState().unexpectedPassTests, 'Tests expected to fail but passed', 'passes-table');
    1488 
    1489     if (globalState().hasHttpTests) {
    1490         html += '<p>httpd access log: <a href="access_log.txt">access_log.txt</a></p>' +
    1491             '<p>httpd error log: <a href="error_log.txt">error_log.txt</a></p>';
    1492     }
    1493 
    1494     document.getElementById('main-content').innerHTML = html + '</div>';
    1495 
    1496     if (document.getElementById('results-table')) {
    1497         document.getElementById('results-table').addEventListener('click', TableSorter.handleClick, false);
    1498         TableSorter.sortColumn(0);
    1499         if (!globalState().results.uses_expectations_file)
    1500             parentOfType(document.getElementById('unexpected-results'), 'label').style.display = 'none';
    1501         if (!globalState().hasTextFailures)
    1502             document.getElementById('text-results-header').textContent = '';
    1503         if (!globalState().hasImageFailures) {
    1504             document.getElementById('image-results-header').textContent = '';
    1505             parentOfType(document.getElementById('toggle-images'), 'label').style.display = 'none';
    1506         }
    1507     }
    1508 
    1509     updateTestlistCounts();
    1510 
    1511     TestNavigator.reset();
     1883    let container = document.getElementById('main-content');
     1884
     1885    controller = new TestResultsController(container, testResults);
     1886    pixelZoomer = new PixelZoomer();
     1887    testNavigator = new TestNavigator();
     1888
    15121889    OptionWriter.apply();
    15131890}
     1891
     1892window.addEventListener('load', generatePage, false);
     1893
    15141894</script>
    1515 <body onload="generatePage()">
     1895<body>
    15161896   
    15171897    <div class="content-container">
    15181898        <div id="toolbar" class="floating-panel">
    15191899        <div class="note">Use the i, j, k and l keys to navigate, e, c to expand and collapse, and f to flag</div>
    1520         <a href="javascript:void()" onclick="expandAllExpectations()">expand all</a>
    1521         <a href="javascript:void()" onclick="collapseAllExpectations()">collapse all</a>
    1522         <a href="javascript:void()" id=options-link onclick="toggleOptionsMenu()">options</a>
     1900        <a class="clickable" onclick="controller.expandAllExpectations()">expand all</a>
     1901        <a class="clickable" onclick="controller.collapseAllExpectations()">collapse all</a>
     1902        <a class="clickable" id=options-link onclick="controller.toggleOptionsMenu()">options</a>
    15231903        <div id="options-menu" class="hidden-menu">
    1524             <label><input id="unexpected-results" type="checkbox" checked onchange="handleUnexpectedResultsChange()">Only unexpected results</label>
    1525             <label><input id="toggle-images" type="checkbox" checked onchange="handleToggleImagesChange()">Toggle images</label>
    1526             <label title="Use newlines instead of spaces to separate flagged tests"><input id="use-newlines" type="checkbox" checked onchange="handleToggleUseNewlines()">Use newlines in flagged list</input>
     1904            <label><input id="unexpected-results" type="checkbox" checked onchange="controller.handleUnexpectedResultsChange()">Only unexpected results</label>
     1905            <label><input id="toggle-images" type="checkbox" checked onchange="controller.handleToggleImagesChange()">Toggle images</label>
     1906            <label title="Use newlines instead of spaces to separate flagged tests"><input id="use-newlines" type="checkbox" checked onchange="controller.handleToggleUseNewlines()">Use newlines in flagged list</label>
    15271907        </div>
    15281908    </div>
Note: See TracChangeset for help on using the changeset viewer.