chromium/third_party/blink/web_tests/resources/testharnessreport.js

/*
 * This file is intended for vendors to implement code needed to integrate
 * testharness.js tests with their own test systems.
 *
 * Typically such integration will attach callbacks when each test is
 * has run, using add_result_callback(callback(test)), or when the whole test
 * file has completed, using add_completion_callback(callback(tests,
 * harness_status)).
 *
 * For more documentation about the callback functions and the
 * parameters they are called with, see testharness.js, or the docs at:
 * https://web-platform-tests.org/writing-tests/testharness-api.html
 */

(function() {

    let outputDocument = document;
    let didDispatchLoadEvent = false;
    let localPathRegExp = undefined;

    // Setup for Blink JavaScript tests. self.testRunner is expected to be
    // present when tests are run.
    if (self.testRunner) {
        testRunner.dumpAsText();
        testRunner.waitUntilDone();
        testRunner.setPopupBlockingEnabled(false);
        testRunner.setDumpJavaScriptDialogs(false);

        // Some tests intentionally load mixed content in order to test the
        // referrer policy, so WebKitAllowRunningInsecureContent must be set
        // to true or else the load would be blocked.
        const paths = [
            'service-workers/service-worker/fetch-event-referrer-policy.https.html',
            'beacon/headers/header-referrer-no-referrer-when-downgrade.https.html',
            'beacon/headers/header-referrer-strict-origin-when-cross-origin.https.html',
            'beacon/headers/header-referrer-strict-origin.https.html',
            'beacon/headers/header-referrer-unsafe-url.https.html',
        ];
        for (const path of paths) {
          if (document.URL.endsWith(path)) {
            testRunner.overridePreference('WebKitAllowRunningInsecureContent', true);
            break;
          }
        }
    }

    if (document.URL.startsWith('file:///')) {
        const index = document.URL.indexOf('/external/wpt');
        if (index >= 0) {
            const localPath = document.URL.substring(
                'file:///'.length, index + '/external/wpt'.length);
            localPathRegExp = new RegExp(localPath.replace(/(\W)/g, '\\$1'), 'g');
        }
    }

    window.addEventListener('load', loadCallback, {'once': true});

    setup({
        // The default output formats test results into an HTML table, but for
        // the Blink layout test runner, we dump the results as text in the
        // completion callback, so we disable the default output.
        'output': false,
        // The Blink layout test runner has its own timeout mechanism.
        'explicit_timeout': true
    });

    add_start_callback(startCallback);
    add_completion_callback(completionCallback);

    /** Loads an automation script if necessary. */
    function loadCallback() {
        didDispatchLoadEvent = true;
        if (isWPTManualTest()) {
            setTimeout(loadAutomationScript, 0);
        }
    }

    /** Checks whether the current path is a manual test in WPT. */
    function isWPTManualTest() {
        // Here we assume that if wptserve is running, then the hostname
        // is web-platform.test.
        const path = location.pathname;
        if (location.hostname == 'web-platform.test' &&
            /.*-manual\.[^-]+$/.test(path)) {
            return true;
        }
        // If the file is loaded locally via file://, it must include
        // the wpt directory in the path.
        return /\/external\/wpt\/.*-manual\.[^-]+$/.test(path);
    }

    /** Loads the WPT automation script for the current test, if applicable. */
    function loadAutomationScript() {
        const pathAndBase = pathAndBaseNameInWPT();
        if (!pathAndBase) {
            return;
        }
        let automationPath = location.pathname.replace(
            /\/external\/wpt\/.*$/, '/external/wpt_automation');
        if (location.hostname == 'web-platform.test') {
            automationPath = '/wpt_automation';
        }

        // Export importAutomationScript for use by the automation scripts.
        window.importAutomationScript = function(relativePath) {
            const script = document.createElement('script');
            script.src = automationPath + relativePath;
            document.head.appendChild(script);
        };

        let src;
        if (pathAndBase.startsWith('/fullscreen/')) {
            // Fullscreen tests all use the same automation script.
            src = automationPath + '/fullscreen/auto-click.js';
        } else if (pathAndBase.startsWith('/file-system-access/local_')) {
            // local_ File System Access tests all use the same automation script.
            src = automationPath + '/file-system-access/auto-pick-folder.js';
        } else if (pathAndBase.startsWith('/file-system-access/')) {
            // Per-test automation scripts.
            src = automationPath + pathAndBase + '-automation.sub.js';
        } else if (
            pathAndBase.startsWith('/css/') ||
            pathAndBase.startsWith('/pointerevents/') ||
            pathAndBase.startsWith('/uievents/') ||
            pathAndBase.startsWith('/html/') ||
            pathAndBase.startsWith('/input-events/') ||
            pathAndBase.startsWith('/css/selectors/') ||
            pathAndBase.startsWith('/css/cssom-view/') ||
            pathAndBase.startsWith('/css/css-scroll-snap/') ||
            pathAndBase.startsWith('/dom/events/') ||
            pathAndBase.startsWith('/feature-policy/experimental-features/') ||
            pathAndBase.startsWith('/permissions-policy/experimental-features/')) {
            // Per-test automation scripts.
            src = automationPath + pathAndBase + '-automation.js';
        } else {
            return;
        }
        const script = document.createElement('script');
        script.src = src;
        document.head.appendChild(script);
    }

    /**
     * Sets the output document based on the given properties.
     * Usually the output document is the current document, but it could be
     * a separate document in some cases.
     */
    function startCallback(properties) {
        if (properties.output_document) {
            outputDocument = properties.output_document;
        }
    }

    /**
     * Adds results to the page in a manner that allows dumpAsText to produce
     * readable test results.
     */
    function completionCallback(tests, harness_status) {
        const xhtmlNS = 'http://www.w3.org/1999/xhtml';

        // Create element to hold results.
        const resultsElement = outputDocument.createElementNS(xhtmlNS, 'pre');
        resultsElement.style.whiteSpace = 'pre-wrap';
        resultsElement.style.lineHeight = '1.5';

        // Declare result string.
        let resultStr = 'This is a testharness.js-based test.\n';

        // Check harness_status.  If it is not 0, tests did not execute
        // correctly, output the error code and message.
        if (harness_status.status != 0) {
            resultStr += `Harness Error. ` +
                `harness_status.status = ${harness_status.status} , ` +
                `harness_status.message = ${harness_status.message}\n`;
        }

        // Output failure metrics if there are many.
        const resultCounts = countResultTypes(tests);
        if (outputDocument.URL.indexOf('://web-platform.test') >= 0 &&
            tests.length >= 50 &&
            (resultCounts[1] || resultCounts[2] || resultCounts[3])) {

            resultStr += failureMetricSummary(resultCounts);
        }

        resultStr += testOutput(tests);

        resultStr += 'Harness: the test ran to completion.\n';

        resultsElement.textContent = resultStr;

        function done() {
            let body = null;
            if (outputDocument.body && outputDocument.body.tagName == 'BODY' &&
                outputDocument.body.namespaceURI == xhtmlNS) {
                body = outputDocument.body;
            }
            // A temporary workaround since |window.self| property lookup starts
            // failing if the frame is detached. |outputDocument| may be an
            // ancestor of |self| so clearing |textContent| may detach |self|.
            // To get around this, cache window.self now and use the cached
            // value.
            // TODO(dcheng): Remove this hack after fixing window/self/frames
            // lookup in https://crbug.com/618672
            const cachedSelf = window.self;
            if (cachedSelf.testRunner) {
                // The following DOM operations may show console messages.  We
                // suppress them because they are not related to the running
                // test.
                testRunner.setDumpConsoleMessages(false);

                // Anything in the body isn't part of the output and so should
                // be hidden from the text dump.
                if (body) {
                    body.textContent = '';
                }
            }

            // Add the results element to the output document.
            if (!body) {
                // |outputDocument| might be an SVG document.
                if (outputDocument.documentElement) {
                    outputDocument.documentElement.remove();
                }
                let html = outputDocument.createElementNS(xhtmlNS, 'html');
                outputDocument.appendChild(html);
                body = outputDocument.createElementNS(xhtmlNS, 'body');
                body.setAttribute('style', 'white-space:pre;');
                html.appendChild(body);
            }
            outputDocument.body.appendChild(resultsElement);

            // IFrames running tests should not complete the harness as the parent
            // page will.
            let shouldCompleteHarness = (window.self == window.top);
            if (cachedSelf.testRunner && shouldCompleteHarness) {
                testRunner.notifyDone();
            }
        }

        if (didDispatchLoadEvent || outputDocument.readyState != 'loading') {
            // This function might not be the last completion callback, and
            // another completion callback might generate more results.
            // So, we don't dump the results immediately.
            setTimeout(done, 0);
        } else {
            // Parsing the test HTML isn't finished yet.
            window.addEventListener('load', done);
        }
    }

    /**
     * Returns a directory part relative to WPT root and a basename part of the
     * current test. e.g.
     * Current test: file:///.../LayoutTests/external/wpt/pointerevents/foobar.html
     * Output: "/pointerevents/foobar"
     */
    function pathAndBaseNameInWPT() {
        const path = location.pathname;
        let matches;
        if (location.hostname == 'web-platform.test') {
            matches = path.match(/^(\/.*)\.html$/);
            return matches ? matches[1] : null;
        }
        matches = path.match(/external\/wpt(\/.*)\.html$/);
        return matches ? matches[1] : null;
    }

    /** Converts the testharness test status into the corresponding string. */
    function convertResult(resultStatus) {
        let retVal = '';
        switch (resultStatus) {
            case 0:
                retVal = 'PASS';
                break;
            case 1:
                retVal = 'FAIL';
                break;
            case 2:
                retVal = 'TIMEOUT';
                break;
            case 3:
                retVal = 'NOTRUN';
                break;
            case 4:
                retVal = 'PRECONDITION_FAILED';
                break;
            default:
                retVal = 'NOTRUN';
                break;
        }
        return '[' + retVal + ']';
    }

    function testOutput(tests) {
        let testResults = '';
        window.tests = tests;
        for (let test of tests) {
            testResults += resultLine(test);
        }
        return testResults;
    }

    function resultLine(test) {
        if (test.status == 0) {
            return '';
        }
        let result = `${convertResult(test.status)} ${sanitize(test.name)}`;
        // include error message when test result is FAIL or PRECONDITION_FAILED
        if (test.message) {
            if (test.status == 1 || test.status == 4) {
                result += '\n  ' + sanitize(test.message).trim();
            }
        }
        return result + '\n';
    }

    /** Prepares the given text for display in test results. */
    function sanitize(text) {
        if (!text) {
            return '';
        }
        // Change each '\' to '\\'
        text = text.replace(/\\/g, '\\\\');
        // Escape null characters, otherwise diff will think the file is binary.
        text = text.replace(/\0/g, '\\0');
        // Escape some special characters to improve readability of the output.
        text = text.replace(/\r/g, '\\r');
        text = text.replace(/\n/g, '\\n');

        // Replace machine-dependent path with "...".
        if (localPathRegExp) {
            text = text.replace(localPathRegExp, '...');
        }
        return text;
    }

    function countResultTypes(tests) {
        const resultCounts = [0, 0, 0, 0];
        for (let test of tests) {
            resultCounts[test.status]++;
        }
        return resultCounts;
    }

    function failureMetricSummary(resultCounts) {
        return `Found ${resultCounts[1]} FAIL,` +
            ` ${resultCounts[2]} TIMEOUT,` +
            ` ${resultCounts[3]} NOTRUN.\n`;
    }

})();