blob: 034aa81a84dd5681e50296670546a86c3178e67d [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview using private properties isn't a Closure violation in tests.
* @suppress {accessControls}
*/
/* eslint-disable no-console */
/** @type {!{logToStderr: function(), navigateSecondaryWindow: function(string), notifyDone: function()}|undefined} */
self.testRunner;
/**
* Only tests in /LayoutTests/http/tests/devtools/startup/ need to call
* this method because these tests need certain activities to be exercised
* in the inspected page prior to the DevTools session.
* @param {string} path
* @return {!Promise<undefined>}
*/
TestRunner.setupStartupTest = function(path) {
const absoluteURL = TestRunner.url(path);
self.testRunner.navigateSecondaryWindow(absoluteURL);
return new Promise(f => TestRunner._startupTestSetupFinished = () => {
TestRunner._initializeTargetForStartupTest();
delete TestRunner._startupTestSetupFinished;
f();
});
};
TestRunner._executeTestScript = function() {
const testScriptURL = /** @type {string} */ (Runtime.queryParam('test'));
fetch(testScriptURL)
.then(data => data.text())
.then(testScript => {
if (TestRunner._isDebugTest()) {
TestRunner.addResult = console.log;
TestRunner.completeTest = () => console.log('Test completed');
// Auto-start unit tests
if (!self.testRunner)
eval(`(function test(){${testScript}})()\n//# sourceURL=${testScriptURL}`);
else
self.eval(`function test(){${testScript}}\n//# sourceURL=${testScriptURL}`);
return;
}
// Convert the test script into an expression (if needed)
testScript = testScript.trimRight();
if (testScript.endsWith(';'))
testScript = testScript.slice(0, testScript.length - 1);
(async function() {
try {
await eval(testScript + `\n//# sourceURL=${testScriptURL}`);
} catch (err) {
TestRunner.addResult('TEST ENDED EARLY DUE TO UNCAUGHT ERROR:');
TestRunner.addResult(err && err.stack || err);
TestRunner.addResult('=== DO NOT COMMIT THIS INTO -expected.txt ===');
TestRunner.completeTest();
}
})();
})
.catch(error => {
TestRunner.addResult(`Unable to execute test script because of error: ${error}`);
TestRunner.completeTest();
});
};
/** @type {!Array<string>} */
TestRunner._results = [];
TestRunner.completeTest = function() {
TestRunner.flushResults();
self.testRunner.notifyDone();
};
/**
* @suppressGlobalPropertiesCheck
*/
TestRunner.flushResults = function() {
Array.prototype.forEach.call(document.documentElement.childNodes, x => x.remove());
const outputElement = document.createElement('div');
// Support for svg - add to document, not body, check for style.
if (outputElement.style) {
outputElement.style.whiteSpace = 'pre';
outputElement.style.height = '10px';
outputElement.style.overflow = 'hidden';
}
document.documentElement.appendChild(outputElement);
for (let i = 0; i < TestRunner._results.length; i++) {
outputElement.appendChild(document.createTextNode(TestRunner._results[i]));
outputElement.appendChild(document.createElement('br'));
}
TestRunner._results = [];
};
/**
* @param {*} text
*/
TestRunner.addResult = function(text) {
TestRunner._results.push(String(text));
};
/**
* @param {!Array<string>} textArray
*/
TestRunner.addResults = function(textArray) {
if (!textArray)
return;
for (let i = 0, size = textArray.length; i < size; ++i)
TestRunner.addResult(textArray[i]);
};
/**
* @param {!Array<function()>} tests
*/
TestRunner.runTests = function(tests) {
nextTest();
function nextTest() {
const test = tests.shift();
if (!test) {
TestRunner.completeTest();
return;
}
TestRunner.addResult('\ntest: ' + test.name);
let testPromise = test();
if (!(testPromise instanceof Promise))
testPromise = Promise.resolve();
testPromise.then(nextTest);
}
};
/**
* @param {!Object} receiver
* @param {string} methodName
* @param {!Function} override
* @param {boolean=} opt_sticky
*/
TestRunner.addSniffer = function(receiver, methodName, override, opt_sticky) {
override = TestRunner.safeWrap(override);
const original = receiver[methodName];
if (typeof original !== 'function')
throw new Error('Cannot find method to override: ' + methodName);
receiver[methodName] = function(var_args) {
let result;
try {
result = original.apply(this, arguments);
} finally {
if (!opt_sticky)
receiver[methodName] = original;
}
// In case of exception the override won't be called.
try {
Array.prototype.push.call(arguments, result);
override.apply(this, arguments);
} catch (e) {
throw new Error('Exception in overriden method \'' + methodName + '\': ' + e);
}
return result;
};
};
/**
* @param {!Object} receiver
* @param {string} methodName
* @return {!Promise<*>}
*/
TestRunner.addSnifferPromise = function(receiver, methodName) {
return new Promise(function(resolve, reject) {
const original = receiver[methodName];
if (typeof original !== 'function') {
reject('Cannot find method to override: ' + methodName);
return;
}
receiver[methodName] = function(var_args) {
let result;
try {
result = original.apply(this, arguments);
} finally {
receiver[methodName] = original;
}
// In case of exception the override won't be called.
try {
Array.prototype.push.call(arguments, result);
resolve.apply(this, arguments);
} catch (e) {
reject('Exception in overridden method \'' + methodName + '\': ' + e);
TestRunner.completeTest();
}
return result;
};
});
};
/** @type {function():void} */
TestRunner._resolveOnFinishInits;
/**
* @param {string} module
* @return {!Promise<undefined>}
*/
TestRunner.loadModule = async function(module) {
const promise = new Promise(resolve => TestRunner._resolveOnFinishInits = resolve);
await self.runtime.loadModulePromise(module);
if (!TestRunner._pendingInits)
return;
return promise;
};
/**
* @param {string} panel
* @return {!Promise.<?UI.Panel>}
*/
TestRunner.showPanel = function(panel) {
return UI.viewManager.showView(panel);
};
/**
* @param {string} key
* @param {boolean=} ctrlKey
* @param {boolean=} altKey
* @param {boolean=} shiftKey
* @param {boolean=} metaKey
* @return {!KeyboardEvent}
*/
TestRunner.createKeyEvent = function(key, ctrlKey, altKey, shiftKey, metaKey) {
return new KeyboardEvent('keydown', {
key: key,
bubbles: true,
cancelable: true,
ctrlKey: !!ctrlKey,
altKey: !!altKey,
shiftKey: !!shiftKey,
metaKey: !!metaKey
});
};
/**
* @param {!Function|undefined} func
* @param {!Function=} onexception
* @return {!Function}
*/
TestRunner.safeWrap = function(func, onexception) {
/**
* @this {*}
*/
function result() {
if (!func)
return;
const wrapThis = this;
try {
return func.apply(wrapThis, arguments);
} catch (e) {
TestRunner.addResult('Exception while running: ' + func + '\n' + (e.stack || e));
if (onexception)
TestRunner.safeWrap(onexception)();
else
TestRunner.completeTest();
}
}
return result;
};
/**
* @param {!Node} node
* @return {string}
*/
TestRunner.textContentWithLineBreaks = function(node) {
function padding(currentNode) {
let result = 0;
while (currentNode && currentNode !== node) {
if (currentNode.nodeName === 'OL' &&
!(currentNode.classList && currentNode.classList.contains('object-properties-section')))
++result;
currentNode = currentNode.parentNode;
}
return Array(result * 4 + 1).join(' ');
}
let buffer = '';
let currentNode = node;
let ignoreFirst = false;
while (currentNode.traverseNextNode(node)) {
currentNode = currentNode.traverseNextNode(node);
if (currentNode.nodeType === Node.TEXT_NODE) {
buffer += currentNode.nodeValue;
} else if (currentNode.nodeName === 'LI' || currentNode.nodeName === 'TR') {
if (!ignoreFirst)
buffer += '\n' + padding(currentNode);
else
ignoreFirst = false;
} else if (currentNode.nodeName === 'STYLE') {
currentNode = currentNode.traverseNextNode(node);
continue;
} else if (currentNode.classList && currentNode.classList.contains('object-properties-section')) {
ignoreFirst = true;
}
}
return buffer;
};
/**
* @param {!Node} node
* @return {string}
*/
TestRunner.textContentWithoutStyles = function(node) {
let buffer = '';
let currentNode = node;
while (currentNode.traverseNextNode(node)) {
currentNode = currentNode.traverseNextNode(node);
if (currentNode.nodeType === Node.TEXT_NODE)
buffer += currentNode.nodeValue;
else if (currentNode.nodeName === 'STYLE')
currentNode = currentNode.traverseNextNode(node);
}
return buffer;
};
/**
* @param {!SDK.Target} target
*/
TestRunner._setupTestHelpers = function(target) {
TestRunner.BrowserAgent = target.browserAgent();
TestRunner.CSSAgent = target.cssAgent();
TestRunner.DeviceOrientationAgent = target.deviceOrientationAgent();
TestRunner.DOMAgent = target.domAgent();
TestRunner.DOMDebuggerAgent = target.domdebuggerAgent();
TestRunner.DebuggerAgent = target.debuggerAgent();
TestRunner.EmulationAgent = target.emulationAgent();
TestRunner.HeapProfilerAgent = target.heapProfilerAgent();
TestRunner.InspectorAgent = target.inspectorAgent();
TestRunner.NetworkAgent = target.networkAgent();
TestRunner.OverlayAgent = target.overlayAgent();
TestRunner.PageAgent = target.pageAgent();
TestRunner.ProfilerAgent = target.profilerAgent();
TestRunner.RuntimeAgent = target.runtimeAgent();
TestRunner.TargetAgent = target.targetAgent();
TestRunner.networkManager = target.model(SDK.NetworkManager);
TestRunner.securityOriginManager = target.model(SDK.SecurityOriginManager);
TestRunner.resourceTreeModel = target.model(SDK.ResourceTreeModel);
TestRunner.debuggerModel = target.model(SDK.DebuggerModel);
TestRunner.runtimeModel = target.model(SDK.RuntimeModel);
TestRunner.domModel = target.model(SDK.DOMModel);
TestRunner.domDebuggerModel = target.model(SDK.DOMDebuggerModel);
TestRunner.cssModel = target.model(SDK.CSSModel);
TestRunner.cpuProfilerModel = target.model(SDK.CPUProfilerModel);
TestRunner.overlayModel = target.model(SDK.OverlayModel);
TestRunner.serviceWorkerManager = target.model(SDK.ServiceWorkerManager);
TestRunner.tracingManager = target.model(SDK.TracingManager);
TestRunner.mainTarget = target;
};
/**
* @param {string} code
* @return {!Promise<*>}
*/
TestRunner.evaluateInPageRemoteObject = async function(code) {
const response = await TestRunner._evaluateInPage(code);
return TestRunner.runtimeModel.createRemoteObject(response.result);
};
/**
* @param {string} code
* @param {function(*, !Protocol.Runtime.ExceptionDetails=):void} callback
*/
TestRunner.evaluateInPage = async function(code, callback) {
const response = await TestRunner._evaluateInPage(code);
TestRunner.safeWrap(callback)(response.result.value, response.exceptionDetails);
};
/** @type {number} */
TestRunner._evaluateInPageCounter = 0;
/**
* @param {string} code
* @return {!Promise<{response: !SDK.RemoteObject,
* exceptionDetails: (!Protocol.Runtime.ExceptionDetails|undefined)}>}
*/
TestRunner._evaluateInPage = async function(code) {
const lines = new Error().stack.split('at ');
// Handles cases where the function is safe wrapped
const testScriptURL = /** @type {string} */ (Runtime.queryParam('test'));
const functionLine = lines.reduce((acc, line) => line.includes(testScriptURL) ? line : acc, lines[lines.length - 2]);
const components = functionLine.trim().split('/');
const source = components[components.length - 1].slice(0, -1).split(':');
const fileName = source[0];
const sourceURL = `test://evaluations/${TestRunner._evaluateInPageCounter++}/` + fileName;
const lineOffset = parseInt(source[1], 10);
code = '\n'.repeat(lineOffset - 1) + code;
if (code.indexOf('sourceURL=') === -1)
code += `//# sourceURL=${sourceURL}`;
const response = await TestRunner.RuntimeAgent.invoke_evaluate({expression: code, objectGroup: 'console'});
const error = response[Protocol.Error];
if (error) {
TestRunner.addResult('Error: ' + error);
TestRunner.completeTest();
return;
}
return response;
};
/**
* Doesn't append sourceURL to snippets evaluated in inspected page
* to avoid churning test expectations
* @param {string} code
* @return {!Promise<*>}
*/
TestRunner.evaluateInPageAnonymously = async function(code) {
const response = await TestRunner.RuntimeAgent.invoke_evaluate({expression: code, objectGroup: 'console'});
if (!response[Protocol.Error])
return response.result.value;
TestRunner.addResult(
'Error: ' +
(response.exceptionDetails && response.exceptionDetails.text || 'exception from evaluateInPageAnonymously.'));
TestRunner.completeTest();
};
/**
* @param {string} code
* @return {!Promise<*>}
*/
TestRunner.evaluateInPagePromise = function(code) {
return new Promise(success => TestRunner.evaluateInPage(code, success));
};
/**
* @param {string} code
* @return {!Promise<*>}
*/
TestRunner.evaluateInPageAsync = async function(code) {
const response = await TestRunner.RuntimeAgent.invoke_evaluate(
{expression: code, objectGroup: 'console', includeCommandLineAPI: false, awaitPromise: true});
const error = response[Protocol.Error];
if (!error && !response.exceptionDetails)
return response.result.value;
TestRunner.addResult(
'Error: ' +
(error || response.exceptionDetails && response.exceptionDetails.text || 'exception while evaluation in page.'));
TestRunner.completeTest();
};
/**
* @param {string} name
* @param {!Array<*>} args
* @return {!Promise<*>}
*/
TestRunner.callFunctionInPageAsync = function(name, args) {
args = args || [];
return TestRunner.evaluateInPageAsync(name + '(' + args.map(a => JSON.stringify(a)).join(',') + ')');
};
/**
* @param {string} code
*/
TestRunner.evaluateInPageWithTimeout = function(code) {
// FIXME: we need a better way of waiting for chromium events to happen
TestRunner.evaluateInPageAnonymously('setTimeout(unescape(\'' + escape(code) + '\'), 1)');
};
/**
* @param {function():*} func
* @param {function(*):void} callback
*/
TestRunner.evaluateFunctionInOverlay = function(func, callback) {
const expression = 'internals.evaluateInInspectorOverlay("(" + ' + func + ' + ")()")';
const mainContext = TestRunner.runtimeModel.executionContexts()[0];
mainContext
.evaluate(
{
expression: expression,
objectGroup: '',
includeCommandLineAPI: false,
silent: false,
returnByValue: true,
generatePreview: false
},
/* userGesture */ false, /* awaitPromise*/ false)
.then(result => void callback(result.object.value));
};
/**
* @param {boolean} passCondition
* @param {string} failureText
*/
TestRunner.check = function(passCondition, failureText) {
if (!passCondition)
TestRunner.addResult('FAIL: ' + failureText);
};
/**
* @param {!Function} callback
*/
TestRunner.deprecatedRunAfterPendingDispatches = function(callback) {
const targets = SDK.targetManager.targets();
const promises = targets.map(target => new Promise(resolve => target._deprecatedRunAfterPendingDispatches(resolve)));
Promise.all(promises).then(TestRunner.safeWrap(callback));
};
/**
* This ensures a base tag is set so all DOM references
* are relative to the test file and not the inspected page
* (i.e. http/tests/devtools/resources/inspected-page.html).
* @param {string} html
* @return {!Promise<*>}
*/
TestRunner.loadHTML = function(html) {
if (!html.includes('<base')) {
// <!DOCTYPE...> tag needs to be first
const doctypeRegex = /(<!DOCTYPE.*?>)/i;
const baseTag = `<base href="${TestRunner.url()}">`;
if (html.match(doctypeRegex))
html = html.replace(doctypeRegex, '$1' + baseTag);
else
html = baseTag + html;
}
html = html.replace(/'/g, '\\\'').replace(/\n/g, '\\n');
return TestRunner.evaluateInPageAnonymously(`document.write(\`${html}\`);document.close();`);
};
/**
* @param {string} path
* @return {!Promise<*>}
*/
TestRunner.addScriptTag = function(path) {
return TestRunner.evaluateInPageAsync(`
(function(){
let script = document.createElement('script');
script.src = '${path}';
document.head.append(script);
return new Promise(f => script.onload = f);
})();
`);
};
/**
* @param {string} path
* @return {!Promise<*>}
*/
TestRunner.addStylesheetTag = function(path) {
return TestRunner.evaluateInPageAsync(`
(function(){
let link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = '${path}';
link.onload = onload;
document.head.append(link);
let resolve;
let promise = new Promise(r => resolve = r);
function onload() {
// TODO(chenwilliam): It shouldn't be necessary to force
// style recalc here but some tests rely on it.
window.getComputedStyle(document.body).color;
resolve();
}
return promise;
})();
`);
};
/**
* @param {string} path
* @return {!Promise<*>}
*/
TestRunner.addHTMLImport = function(path) {
return TestRunner.evaluateInPageAsync(`
(function(){
let link = document.createElement('link');
link.rel = 'import';
link.href = '${path}';
let promise = new Promise(r => link.onload = r);
document.body.append(link);
return promise;
})();
`);
};
/**
* @param {string} path
* @param {!Object|undefined} options
* @return {!Promise<*>}
*/
TestRunner.addIframe = function(path, options = {}) {
options.id = options.id || '';
options.name = options.name || '';
return TestRunner.evaluateInPageAsync(`
(function(){
let iframe = document.createElement('iframe');
iframe.src = '${path}';
iframe.id = '${options.id}';
iframe.name = '${options.name}';
document.body.appendChild(iframe);
return new Promise(f => iframe.onload = f);
})();
`);
};
/** @type {number} */
TestRunner._pendingInits = 0;
/**
* The old test framework executed certain snippets in the inspected page
* context as part of loading a test helper file.
*
* This is deprecated because:
* 1) it makes the testing API less intuitive (need to read the various *TestRunner.js
* files to know which helper functions are available in the inspected page).
* 2) it complicates the test framework's module loading process.
*
* In most cases, this is used to set up inspected page functions (e.g. makeSimpleXHR)
* which should become a *TestRunner method (e.g. NetworkTestRunner.makeSimpleXHR)
* that calls evaluateInPageAnonymously(...).
* @param {string} code
*/
TestRunner.deprecatedInitAsync = async function(code) {
TestRunner._pendingInits++;
await TestRunner.RuntimeAgent.invoke_evaluate({expression: code, objectGroup: 'console'});
TestRunner._pendingInits--;
if (!TestRunner._pendingInits)
TestRunner._resolveOnFinishInits();
};
/**
* @param {string} title
*/
TestRunner.markStep = function(title) {
TestRunner.addResult('\nRunning: ' + title);
};
TestRunner.startDumpingProtocolMessages = function() {
// TODO(chenwilliam): stop abusing Closure interface which is why
// we need to opt out of type checking here
const untypedConnection = /** @type {*} */ (Protocol.InspectorBackend.Connection);
untypedConnection.prototype._dumpProtocolMessage = self.testRunner.logToStderr.bind(self.testRunner);
Protocol.InspectorBackend.Options.dumpInspectorProtocolMessages = 1;
};
/**
* @param {string} url
* @param {string} content
* @param {!SDK.ResourceTreeFrame} frame
*/
TestRunner.addScriptForFrame = function(url, content, frame) {
content += '\n//# sourceURL=' + url;
const executionContext = TestRunner.runtimeModel.executionContexts().find(context => context.frameId === frame.id);
TestRunner.RuntimeAgent.evaluate(content, 'console', false, false, executionContext.id);
};
TestRunner.formatters = {};
/**
* @param {*} value
* @return {string}
*/
TestRunner.formatters.formatAsTypeName = function(value) {
return '<' + typeof value + '>';
};
/**
* @param {*} value
* @return {string}
*/
TestRunner.formatters.formatAsTypeNameOrNull = function(value) {
if (value === null)
return 'null';
return TestRunner.formatters.formatAsTypeName(value);
};
/**
* @param {*} value
* @return {string|!Date}
*/
TestRunner.formatters.formatAsRecentTime = function(value) {
if (typeof value !== 'object' || !(value instanceof Date))
return TestRunner.formatters.formatAsTypeName(value);
const delta = Date.now() - value;
return 0 <= delta && delta < 30 * 60 * 1000 ? '<plausible>' : value;
};
/**
* @param {string} value
* @return {string}
*/
TestRunner.formatters.formatAsURL = function(value) {
if (!value)
return value;
const lastIndex = value.lastIndexOf('devtools/');
if (lastIndex < 0)
return value;
return '.../' + value.substr(lastIndex);
};
/**
* @param {string} value
* @return {string}
*/
TestRunner.formatters.formatAsDescription = function(value) {
if (!value)
return value;
return '"' + value.replace(/^function [gs]et /, 'function ') + '"';
};
/**
* @typedef {!Object<string, string>}
*/
TestRunner.CustomFormatters;
/**
* @param {!Object} object
* @param {!TestRunner.CustomFormatters=} customFormatters
* @param {string=} prefix
* @param {string=} firstLinePrefix
*/
TestRunner.addObject = function(object, customFormatters, prefix, firstLinePrefix) {
prefix = prefix || '';
firstLinePrefix = firstLinePrefix || prefix;
TestRunner.addResult(firstLinePrefix + '{');
const propertyNames = Object.keys(object);
propertyNames.sort();
for (let i = 0; i < propertyNames.length; ++i) {
const prop = propertyNames[i];
if (!object.hasOwnProperty(prop))
continue;
const prefixWithName = ' ' + prefix + prop + ' : ';
const propValue = object[prop];
if (customFormatters && customFormatters[prop]) {
const formatterName = customFormatters[prop];
if (formatterName !== 'skip') {
const formatter = TestRunner.formatters[formatterName];
TestRunner.addResult(prefixWithName + formatter(propValue));
}
} else {
TestRunner.dump(propValue, customFormatters, ' ' + prefix, prefixWithName);
}
}
TestRunner.addResult(prefix + '}');
};
/**
* @param {!Array} array
* @param {!TestRunner.CustomFormatters=} customFormatters
* @param {string=} prefix
* @param {string=} firstLinePrefix
*/
TestRunner.addArray = function(array, customFormatters, prefix, firstLinePrefix) {
prefix = prefix || '';
firstLinePrefix = firstLinePrefix || prefix;
TestRunner.addResult(firstLinePrefix + '[');
for (let i = 0; i < array.length; ++i)
TestRunner.dump(array[i], customFormatters, prefix + ' ');
TestRunner.addResult(prefix + ']');
};
/**
* @param {!Node} node
*/
TestRunner.dumpDeepInnerHTML = function(node) {
/**
* @param {string} prefix
* @param {!Node} node
*/
function innerHTML(prefix, node) {
const openTag = [];
if (node.nodeType === Node.TEXT_NODE) {
if (!node.parentElement || node.parentElement.nodeName !== 'STYLE')
TestRunner.addResult(node.nodeValue);
return;
}
openTag.push('<' + node.nodeName);
const attrs = node.attributes;
for (let i = 0; attrs && i < attrs.length; ++i)
openTag.push(attrs[i].name + '=' + attrs[i].value);
openTag.push('>');
TestRunner.addResult(prefix + openTag.join(' '));
for (let child = node.firstChild; child; child = child.nextSibling)
innerHTML(prefix + ' ', child);
if (node.shadowRoot)
innerHTML(prefix + ' ', node.shadowRoot);
TestRunner.addResult(prefix + '</' + node.nodeName + '>');
}
innerHTML('', node);
};
/**
* @param {!Node} node
* @return {string}
*/
TestRunner.deepTextContent = function(node) {
if (!node)
return '';
if (node.nodeType === Node.TEXT_NODE && node.nodeValue)
return !node.parentElement || node.parentElement.nodeName !== 'STYLE' ? node.nodeValue : '';
let res = '';
const children = node.childNodes;
for (let i = 0; i < children.length; ++i)
res += TestRunner.deepTextContent(children[i]);
if (node.shadowRoot)
res += TestRunner.deepTextContent(node.shadowRoot);
return res;
};
/**
* @param {*} value
* @param {!TestRunner.CustomFormatters=} customFormatters
* @param {string=} prefix
* @param {string=} prefixWithName
*/
TestRunner.dump = function(value, customFormatters, prefix, prefixWithName) {
prefixWithName = prefixWithName || prefix;
if (prefixWithName && prefixWithName.length > 80) {
TestRunner.addResult(prefixWithName + 'was skipped due to prefix length limit');
return;
}
if (value === null)
TestRunner.addResult(prefixWithName + 'null');
else if (value && value.constructor && value.constructor.name === 'Array')
TestRunner.addArray(/** @type {!Array} */ (value), customFormatters, prefix, prefixWithName);
else if (typeof value === 'object')
TestRunner.addObject(/** @type {!Object} */ (value), customFormatters, prefix, prefixWithName);
else if (typeof value === 'string')
TestRunner.addResult(prefixWithName + '"' + value + '"');
else
TestRunner.addResult(prefixWithName + value);
};
/**
* @param {!UI.TreeElement} treeElement
*/
TestRunner.dumpObjectPropertyTreeElement = function(treeElement) {
const expandedSubstring = treeElement.expanded ? '[expanded]' : '[collapsed]';
TestRunner.addResult(expandedSubstring + ' ' + treeElement.listItemElement.deepTextContent());
for (let i = 0; i < treeElement.childCount(); ++i) {
const property = treeElement.childAt(i).property;
const key = property.name;
const value = property.value._description;
TestRunner.addResult(' ' + key + ': ' + value);
}
};
/**
* @param {symbol} event
* @param {!Common.Object} obj
* @param {function(?):boolean=} condition
* @return {!Promise}
*/
TestRunner.waitForEvent = function(event, obj, condition) {
condition = condition || function() {
return true;
};
return new Promise(resolve => {
obj.addEventListener(event, onEventFired);
/**
* @param {!Common.Event} event
*/
function onEventFired(event) {
if (!condition(event.data))
return;
obj.removeEventListener(event, onEventFired);
resolve(event.data);
}
});
};
/**
* @param {function(!SDK.Target):boolean} filter
* @return {!Promise<!SDK.Target>}
*/
TestRunner.waitForTarget = function(filter) {
filter = filter || (target => true);
for (const target of SDK.targetManager.targets()) {
if (filter(target))
return Promise.resolve(target);
}
return new Promise(fulfill => {
const observer = /** @type {!SDK.TargetManager.Observer} */ ({
targetAdded: function(target) {
if (filter(target)) {
SDK.targetManager.unobserveTargets(observer);
fulfill(target);
}
},
targetRemoved: function() {},
});
SDK.targetManager.observeTargets(observer);
});
};
/**
* @param {!SDK.RuntimeModel} runtimeModel
* @return {!Promise}
*/
TestRunner.waitForExecutionContext = function(runtimeModel) {
if (runtimeModel.executionContexts().length)
return Promise.resolve(runtimeModel.executionContexts()[0]);
return runtimeModel.once(SDK.RuntimeModel.Events.ExecutionContextCreated);
};
/**
* @param {!SDK.ExecutionContext} context
* @return {!Promise}
*/
TestRunner.waitForExecutionContextDestroyed = function(context) {
const runtimeModel = context.runtimeModel;
if (runtimeModel.executionContexts().indexOf(context) === -1)
return Promise.resolve();
return TestRunner.waitForEvent(
SDK.RuntimeModel.Events.ExecutionContextDestroyed, runtimeModel,
destroyedContext => destroyedContext === context);
};
/**
* @param {number} a
* @param {number} b
* @param {string=} message
*/
TestRunner.assertGreaterOrEqual = function(a, b, message) {
if (a < b)
TestRunner.addResult('FAILED: ' + (message ? message + ': ' : '') + a + ' < ' + b);
};
/**
* @param {string} url
* @param {function():void} callback
*/
TestRunner.navigate = function(url, callback) {
TestRunner._pageLoadedCallback = TestRunner.safeWrap(callback);
TestRunner.resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.Load, TestRunner._pageNavigated);
// Note: injected <base> means that url is relative to test
// and not the inspected page
TestRunner.evaluateInPageAnonymously('window.location.replace(\'' + url + '\')');
};
/**
* @return {!Promise}
*/
TestRunner.navigatePromise = function(url) {
return new Promise(fulfill => TestRunner.navigate(url, fulfill));
};
TestRunner._pageNavigated = function() {
TestRunner.resourceTreeModel.removeEventListener(SDK.ResourceTreeModel.Events.Load, TestRunner._pageNavigated);
TestRunner._handlePageLoaded();
};
/**
* @param {function():void} callback
*/
TestRunner.hardReloadPage = function(callback) {
TestRunner._innerReloadPage(true, undefined, callback);
};
/**
* @param {function():void} callback
*/
TestRunner.reloadPage = function(callback) {
TestRunner._innerReloadPage(false, undefined, callback);
};
/**
* @param {(string|undefined)} injectedScript
* @param {function():void} callback
*/
TestRunner.reloadPageWithInjectedScript = function(injectedScript, callback) {
TestRunner._innerReloadPage(false, injectedScript, callback);
};
/**
* @return {!Promise}
*/
TestRunner.reloadPagePromise = function() {
return new Promise(fulfill => TestRunner.reloadPage(fulfill));
};
/**
* @param {boolean} hardReload
* @param {(string|undefined)} injectedScript
* @param {function():void} callback
*/
TestRunner._innerReloadPage = function(hardReload, injectedScript, callback) {
TestRunner._pageLoadedCallback = TestRunner.safeWrap(callback);
TestRunner.resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.Load, TestRunner.pageLoaded);
TestRunner.resourceTreeModel.reloadPage(hardReload, injectedScript);
};
TestRunner.pageLoaded = function() {
TestRunner.resourceTreeModel.removeEventListener(SDK.ResourceTreeModel.Events.Load, TestRunner.pageLoaded);
TestRunner.addResult('Page reloaded.');
TestRunner._handlePageLoaded();
};
TestRunner._handlePageLoaded = async function() {
await TestRunner.waitForExecutionContext(/** @type {!SDK.RuntimeModel} */ (TestRunner.runtimeModel));
if (TestRunner._pageLoadedCallback) {
const callback = TestRunner._pageLoadedCallback;
delete TestRunner._pageLoadedCallback;
callback();
}
};
/**
* @param {function():void} callback
*/
TestRunner.waitForPageLoad = function(callback) {
TestRunner.resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.Load, onLoaded);
function onLoaded() {
TestRunner.resourceTreeModel.removeEventListener(SDK.ResourceTreeModel.Events.Load, onLoaded);
callback();
}
};
/**
* @param {function():void} callback
*/
TestRunner.runWhenPageLoads = function(callback) {
const oldCallback = TestRunner._pageLoadedCallback;
function chainedCallback() {
if (oldCallback)
oldCallback();
callback();
}
TestRunner._pageLoadedCallback = TestRunner.safeWrap(chainedCallback);
};
/**
* @param {!Array<function(function():void)>} testSuite
*/
TestRunner.runTestSuite = function(testSuite) {
const testSuiteTests = testSuite.slice();
function runner() {
if (!testSuiteTests.length) {
TestRunner.completeTest();
return;
}
const nextTest = testSuiteTests.shift();
TestRunner.addResult('');
TestRunner.addResult(
'Running: ' +
/function\s([^(]*)/.exec(nextTest)[1]);
TestRunner.safeWrap(nextTest)(runner);
}
runner();
};
/**
* @param {*} expected
* @param {*} found
* @param {string} message
*/
TestRunner.assertEquals = function(expected, found, message) {
if (expected === found)
return;
let error;
if (message)
error = 'Failure (' + message + '):';
else
error = 'Failure:';
throw new Error(error + ' expected <' + expected + '> found <' + found + '>');
};
/**
* @param {*} found
* @param {string} message
*/
TestRunner.assertTrue = function(found, message) {
TestRunner.assertEquals(true, !!found, message);
};
/**
* @param {!Object} receiver
* @param {string} methodName
* @param {!Function} override
* @param {boolean=} opt_sticky
* @return {!Function}
*/
TestRunner.override = function(receiver, methodName, override, opt_sticky) {
override = TestRunner.safeWrap(override);
const original = receiver[methodName];
if (typeof original !== 'function')
throw new Error('Cannot find method to override: ' + methodName);
receiver[methodName] = function(var_args) {
try {
return override.apply(this, arguments);
} catch (e) {
throw new Error('Exception in overriden method \'' + methodName + '\': ' + e);
} finally {
if (!opt_sticky)
receiver[methodName] = original;
}
};
return original;
};
/**
* @param {string} text
* @return {string}
*/
TestRunner.clearSpecificInfoFromStackFrames = function(text) {
let buffer = text.replace(/\(file:\/\/\/(?:[^)]+\)|[\w\/:-]+)/g, '(...)');
buffer = buffer.replace(/\(http:\/\/(?:[^)]+\)|[\w\/:-]+)/g, '(...)');
buffer = buffer.replace(/\(test:\/\/(?:[^)]+\)|[\w\/:-]+)/g, '(...)');
buffer = buffer.replace(/\(<anonymous>:[^)]+\)/g, '(...)');
buffer = buffer.replace(/VM\d+/g, 'VM');
return buffer.replace(/\s*at[^()]+\(native\)/g, '');
};
TestRunner.hideInspectorView = function() {
UI.inspectorView.element.setAttribute('style', 'display:none !important');
};
/**
* @return {?SDK.ResourceTreeFrame}
*/
TestRunner.mainFrame = function() {
return TestRunner.resourceTreeModel.mainFrame;
};
TestRunner.StringOutputStream = class {
/**
* @param {function(string):void} callback
*/
constructor(callback) {
this._callback = callback;
this._buffer = '';
}
/**
* @param {string} fileName
* @return {!Promise<boolean>}
*/
async open(fileName) {
return true;
}
/**
* @param {string} chunk
*/
async write(chunk) {
this._buffer += chunk;
}
async close() {
this._callback(this._buffer);
}
};
/**
* @template V
*/
TestRunner.MockSetting = class {
/**
* @param {V} value
*/
constructor(value) {
this._value = value;
}
/**
* @return {V}
*/
get() {
return this._value;
}
/**
* @param {V} value
*/
set(value) {
this._value = value;
}
};
/**
* @return {!Array<!Runtime.Module>}
*/
TestRunner.loadedModules = function() {
return self.runtime._modules.filter(module => module._loadedForTest)
.filter(module => module.name().indexOf('test_runner') === -1);
};
/**
* @param {!Array<!Runtime.Module>} relativeTo
* @return {!Array<!Runtime.Module>}
*/
TestRunner.dumpLoadedModules = function(relativeTo) {
const previous = new Set(relativeTo || []);
function moduleSorter(left, right) {
return String.naturalOrderComparator(left._descriptor.name, right._descriptor.name);
}
TestRunner.addResult('Loaded modules:');
const loadedModules = TestRunner.loadedModules().sort(moduleSorter);
for (const module of loadedModules) {
if (previous.has(module))
continue;
TestRunner.addResult(' ' + module._descriptor.name);
}
return loadedModules;
};
/**
* @param {!SDK.Target} target
* @return {boolean}
*/
TestRunner.isDedicatedWorker = function(target) {
return target && !target.hasBrowserCapability() && target.hasJSCapability() && target.hasLogCapability();
};
/**
* @param {!SDK.Target} target
* @return {boolean}
*/
TestRunner.isServiceWorker = function(target) {
return target && !target.hasBrowserCapability() && !target.hasJSCapability() && target.hasNetworkCapability() &&
target.hasTargetCapability();
};
/**
* @param {!SDK.Target} target
* @return {string}
*/
TestRunner.describeTargetType = function(target) {
if (TestRunner.isDedicatedWorker(target))
return 'worker';
if (TestRunner.isServiceWorker(target))
return 'service-worker';
if (!target.parentTarget())
return 'page';
return 'frame';
};
/**
* @param {string} urlSuffix
* @param {!Workspace.projectTypes=} projectType
* @return {!Promise}
*/
TestRunner.waitForUISourceCode = function(urlSuffix, projectType) {
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {boolean}
*/
function matches(uiSourceCode) {
if (projectType && uiSourceCode.project().type() !== projectType)
return false;
if (!projectType && uiSourceCode.project().type() === Workspace.projectTypes.Service)
return false;
if (urlSuffix && !uiSourceCode.url().endsWith(urlSuffix))
return false;
return true;
}
for (const uiSourceCode of Workspace.workspace.uiSourceCodes()) {
if (urlSuffix && matches(uiSourceCode))
return Promise.resolve(uiSourceCode);
}
return TestRunner.waitForEvent(Workspace.Workspace.Events.UISourceCodeAdded, Workspace.workspace, matches);
};
/**
* @param {!Function} callback
*/
TestRunner.waitForUISourceCodeRemoved = function(callback) {
Workspace.workspace.once(Workspace.Workspace.Events.UISourceCodeRemoved).then(callback);
};
/**
* @param {string=} url
* @return {string}
*/
TestRunner.url = function(url = '') {
const testScriptURL = /** @type {string} */ (Runtime.queryParam('test'));
// This handles relative (e.g. "../file"), root (e.g. "/resource"),
// absolute (e.g. "http://", "data:") and empty (e.g. "") paths
return new URL(url, testScriptURL + '/../').href;
};
/**
* @param {string} str
* @param {string} mimeType
* @return {!Promise.<undefined>}
* @suppressGlobalPropertiesCheck
*/
TestRunner.dumpSyntaxHighlight = function(str, mimeType) {
const node = document.createElement('span');
node.textContent = str;
const javascriptSyntaxHighlighter = new UI.SyntaxHighlighter(mimeType, false);
return javascriptSyntaxHighlighter.syntaxHighlightNode(node).then(dumpSyntax);
function dumpSyntax() {
const node_parts = [];
for (let i = 0; i < node.childNodes.length; i++) {
if (node.childNodes[i].getAttribute)
node_parts.push(node.childNodes[i].getAttribute('class'));
else
node_parts.push('*');
}
TestRunner.addResult(str + ': ' + node_parts.join(', '));
}
};
/**
* @param {string} messageType
*/
TestRunner._consoleOutputHook = function(messageType) {
TestRunner.addResult(messageType + ': ' + Array.prototype.slice.call(arguments, 1));
};
/**
* This monkey patches console functions in DevTools context so the console
* messages are shown in the right places, instead of having all of the console
* messages printed at the top of the test expectation file (default behavior).
*/
TestRunner._printDevToolsConsole = function() {
if (TestRunner._isDebugTest())
return;
console.log = TestRunner._consoleOutputHook.bind(TestRunner, 'log');
console.error = TestRunner._consoleOutputHook.bind(TestRunner, 'error');
console.info = TestRunner._consoleOutputHook.bind(TestRunner, 'info');
};
/**
* @param {string} querySelector
*/
TestRunner.dumpInspectedPageElementText = async function(querySelector) {
const value = await TestRunner.evaluateInPageAsync(`document.querySelector('${querySelector}').innerText`);
TestRunner.addResult(value);
};
/** @type {boolean} */
TestRunner._startedTest = false;
/**
* @implements {SDK.TargetManager.Observer}
*/
TestRunner._TestObserver = class {
/**
* @param {!SDK.Target} target
* @override
*/
targetAdded(target) {
if (TestRunner._startedTest)
return;
TestRunner._startedTest = true;
TestRunner._setupTestHelpers(target);
if (TestRunner._isStartupTest())
return;
TestRunner
.loadHTML(`
<head>
<base href="${TestRunner.url()}">
</head>
<body>
</body>
`).then(() => TestRunner._executeTestScript());
}
/**
* @param {!SDK.Target} target
* @override
*/
targetRemoved(target) {
}
};
/**
* @return {boolean}
*/
TestRunner._isDebugTest = function() {
return !self.testRunner || !!Runtime.queryParam('debugFrontend');
};
/**
* @return {boolean}
*/
TestRunner._isStartupTest = function() {
return Runtime.queryParam('test').includes('/startup/');
};
(async function() {
/**
* @param {string|!Event} message
* @param {string} source
* @param {number} lineno
* @param {number} colno
* @param {!Error} error
*/
function completeTestOnError(message, source, lineno, colno, error) {
TestRunner.addResult('TEST ENDED IN ERROR: ' + error.stack);
TestRunner.completeTest();
}
self['onerror'] = completeTestOnError;
TestRunner._printDevToolsConsole();
SDK.targetManager.observeTargets(new TestRunner._TestObserver());
if (!TestRunner._isStartupTest())
return;
/**
* Startup test initialization:
* 1. Wait for DevTools app UI to load
* 2. Execute test script, the first line will be TestRunner.setupStartupTest(...) which:
* A. Navigate secondary window
* B. After preconditions occur, secondary window calls testRunner.inspectSecondaryWindow()
* 3. Backend executes TestRunner._startupTestSetupFinished() which calls _initializeTarget()
*/
TestRunner._initializeTargetForStartupTest =
TestRunner.override(Main.Main._instanceForTest, '_initializeTarget', () => undefined)
.bind(Main.Main._instanceForTest);
await TestRunner.addSnifferPromise(Main.Main._instanceForTest, '_showAppUI');
TestRunner._executeTestScript();
})();