blob: b1e91ff2e706726774326c290717ae3321b2dc4e [file] [log] [blame]
/*
* 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.setCanOpenWindows();
testRunner.setCloseRemainingWindowsWhenComplete(true);
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(\.sub)?(\.https)?(\.tentative)?\.html$/.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(\.sub)?(\.https)?(\.tentative)?\.html$/.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('/css/') ||
pathAndBase.startsWith('/pointerevents/') ||
pathAndBase.startsWith('/uievents/') ||
pathAndBase.startsWith('/pointerlock/') ||
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/')) {
// 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.
resultCounts = countResultTypes(tests);
if (outputDocument.URL.indexOf('://web-platform.test') >= 0 &&
tests.length >= 50 &&
(resultCounts[1] || resultCounts[2] || resultCounts[3])) {
resultStr += failureMetricSummary(resultCounts);
}
if (outputDocument.URL.indexOf('/html/dom/reflection') >= 0) {
resultStr += compactTestOutput(tests);
} else {
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);
if (cachedSelf.testRunner) {
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) {
switch (resultStatus) {
case 0:
return 'PASS';
case 1:
return 'FAIL';
case 2:
return 'TIMEOUT';
default:
return 'NOTRUN';
}
}
/**
* Returns a compact output for reflection test results.
*
* The reflection tests contain a large number of tests.
* This test output merges PASS lines to make baselines smaller.
*/
function compactTestOutput(tests) {
let testResults = [];
for (let i = 0; i < tests.length; ++i) {
if (tests[i].status == 0) {
const colon = tests[i].name.indexOf(':');
if (colon > 0) {
const prefix = tests[i].name.substring(0, colon + 1);
let j = i + 1;
for (; j < tests.length; ++j) {
if (!tests[j].name.startsWith(prefix) ||
tests[j].status != 0)
break;
}
const numPasses = j - i;
if (numPasses > 1) {
testResults.push(
`${convertResult(tests[i].status)} ` +
`${sanitize(prefix)} ${numPasses} tests\n`);
i = j - 1;
continue;
}
}
}
testResults.push(resultLine(tests[i]));
}
return testResults.join('');
}
function testOutput(tests) {
let testResults = '';
window.tests = tests;
for (let test of tests) {
testResults += resultLine(test);
}
return testResults;
}
function resultLine(test) {
let result = `${convertResult(test.status)} ${sanitize(test.name)}`;
if (test.message) {
result += ' ' + sanitize(test.message).trim();
}
return result + '\n';
}
/** Prepares the given text for display in test results. */
function sanitize(text) {
if (!text) {
return '';
}
// 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');
// 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) {
const total = resultCounts[0] + resultCounts[1] + resultCounts[2] + resultCounts[3];
return `Found ${total} tests;` +
` ${resultCounts[0]} PASS,` +
` ${resultCounts[1]} FAIL,` +
` ${resultCounts[2]} TIMEOUT,` +
` ${resultCounts[3]} NOTRUN.\n`;
}
})();