blob: 532710295a7b5fd93f573f381ef660643f0b83af [file] [log] [blame]
// Copyright 2017 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.
var TestRunner = class {
constructor(testBaseURL, targetBaseURL, log, completeTest, fetch, params) {
this._dumpInspectorProtocolMessages = false;
this._testBaseURL = testBaseURL;
this._targetBaseURL = targetBaseURL;
this._log = log;
this._completeTest = completeTest;
this._fetch = fetch;
this._params = params;
this._browserSession = new TestRunner.Session(this, '');
}
static get stabilizeNames() {
return ['id', 'nodeId', 'objectId', 'scriptId', 'timestamp',
'backendNodeId', 'parentId', 'frameId', 'loaderId', 'baseURL',
'documentURL', 'styleSheetId', 'executionContextId', 'openerId',
'targetId', 'browserContextId', 'sessionId', 'receivedBytes',
'ownerNode', 'guid', 'requestId', 'openerFrameId'];
}
startDumpingProtocolMessages() {
this._dumpInspectorProtocolMessages = true;
};
completeTest() {
this._completeTest.call(null);
}
log(item, title, stabilizeNames) {
if (typeof item === 'object')
return this._logObject(item, title, stabilizeNames);
this._log.call(null, item);
}
params(name) {
return name ? this._params.get(name) : this._params;
}
_logObject(object, title, stabilizeNames = TestRunner.stabilizeNames) {
var lines = [];
function dumpValue(value, prefix, prefixWithName) {
if (typeof value === 'object' && value !== null) {
if (value instanceof Array)
dumpItems(value, prefix, prefixWithName);
else
dumpProperties(value, prefix, prefixWithName);
} else {
lines.push(prefixWithName + String(value).replace(/\n/g, ' '));
}
}
function dumpProperties(object, prefix, firstLinePrefix) {
prefix = prefix || '';
firstLinePrefix = firstLinePrefix || prefix;
lines.push(firstLinePrefix + '{');
var propertyNames = Object.keys(object);
propertyNames.sort();
for (var i = 0; i < propertyNames.length; ++i) {
var name = propertyNames[i];
if (!object.hasOwnProperty(name))
continue;
var prefixWithName = ' ' + prefix + name + ' : ';
var value = object[name];
if (stabilizeNames && stabilizeNames.includes(name))
value = `<${typeof value}>`;
dumpValue(value, ' ' + prefix, prefixWithName);
}
lines.push(prefix + '}');
}
function dumpItems(object, prefix, firstLinePrefix) {
prefix = prefix || '';
firstLinePrefix = firstLinePrefix || prefix;
lines.push(firstLinePrefix + '[');
for (var i = 0; i < object.length; ++i)
dumpValue(object[i], ' ' + prefix, ' ' + prefix + '[' + i + '] : ');
lines.push(prefix + ']');
}
dumpValue(object, '', title || '');
this._log.call(null, lines.join('\n'));
}
trimURL(url) {
return url.replace(/^.*(([^/]*[/]){3}[^/]*)$/, '...$1');
}
url(relative) {
if (relative.startsWith('http://') || relative.startsWith('https://') || relative.startsWith('file://'))
return relative;
return this._targetBaseURL + relative;
}
async runTestSuite(testSuite) {
for (var test of testSuite) {
this.log('\nRunning test: ' + test.name);
try {
await test();
} catch (e) {
this.log(`Error during test: ${e}\n${e.stack}`);
}
}
this.completeTest();
}
_checkExpectation(fail, name, messageObject) {
if (fail === !!messageObject.error) {
this.log('PASS: ' + name);
return true;
}
this.log('FAIL: ' + name + ': ' + JSON.stringify(messageObject));
this.completeTest();
return false;
}
expectedSuccess(name, messageObject) {
return this._checkExpectation(false, name, messageObject);
}
expectedError(name, messageObject) {
return this._checkExpectation(true, name, messageObject);
}
die(message, error) {
this.log(`${message}: ${error}\n${error.stack}`);
this.completeTest();
throw new Error(message);
}
fail(message) {
this.log('FAIL: ' + message);
this.completeTest();
}
async loadScript(url) {
var source = await this._fetch(this._testBaseURL + url);
return eval(`${source}\n//# sourceURL=${url}`);
};
async loadScriptAbsolute(url) {
var source = await this._fetch(url);
return eval(`${source}\n//# sourceURL=${url}`);
};
async loadScriptModule(path) {
const source = await this._fetch(this._testBaseURL + path);
return new Promise((resolve, reject) => {
const src = URL.createObjectURL(new Blob([source], { type: 'application/javascript' }));
const script = Object.assign(document.createElement('script'), {
src,
type: 'module',
onerror: reject,
onload: resolve
});
document.head.appendChild(script);
})
};
browserP() {
return this._browserSession.protocol;
}
async createPage(options) {
options = options || {};
const browserProtocol = this._browserSession.protocol;
const params = {url: 'about:blank'};
if (options.width)
params.width = options.width;
if (options.height)
params.height = options.height;
if (options.enableBeginFrameControl)
params.enableBeginFrameControl = true;
if (options.createContext) {
const browserContextId = (await browserProtocol.Target.createBrowserContext()).result.browserContextId;
options.browserContextId = browserContextId;
}
const targetId = (await browserProtocol.Target.createTarget(params)).result.targetId;
const page = new TestRunner.Page(this, targetId);
let url = options.url || DevToolsHost.dummyPageURL;
if (!url) {
url = window.location.href;
url = url.substring(0, url.indexOf('inspector-protocol-test.html')) + 'inspector-protocol-page.html';
}
await page.navigate(url);
return page;
}
async _start(description, options) {
try {
if (!description)
throw new Error('Please provide a description for the test!');
this.log(description);
var page = await this.createPage(options);
if (options.html)
await page.loadHTML(options.html);
var session = await page.createSession();
return { page: page, session: session, dp: session.protocol };
} catch (e) {
this.die('Error starting the test', e);
}
};
startBlank(description, options) {
return this._start(description, options || {});
}
startHTML(html, description, options) {
options = options || {};
options.html = html;
return this._start(description, options);
}
startURL(url, description, options) {
options = options || {};
options.url = url;
return this._start(description, options);
}
startWithFrameControl(description, options) {
options = options || {};
options.width = options.width || 800;
options.height = options.height || 600;
options.createContext = true;
options.enableBeginFrameControl = true;
return this._start(description, options);
}
async logStackTrace(debuggers, stackTrace, debuggerId) {
while (stackTrace) {
const {description, callFrames, parent, parentId} = stackTrace;
if (description)
this.log(`--${description}--`);
this.logCallFrames(callFrames);
if (parentId) {
if (parentId.debuggerId)
debuggerId = parentId.debuggerId;
let result = await debuggers.get(debuggerId).getStackTrace({
stackTraceId: parentId
});
stackTrace = result.stackTrace || result.result.stackTrace;
} else {
stackTrace = parent;
}
}
}
_replaceUUID(url) {
const uuidRegex = new RegExp('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}');
return url.replace(uuidRegex, 'UUID');
}
logCallFrames(callFrames) {
for (let frame of callFrames) {
let functionName = frame.functionName || '(anonymous)';
let url = this._replaceUUID(frame.url);
let location = frame.location || frame;
this.log(`${functionName} at ${url}:${
location.lineNumber
}:${location.columnNumber}`);
}
}
};
TestRunner.Page = class {
constructor(testRunner, targetId) {
this._testRunner = testRunner;
this._targetId = targetId;
}
async createSession() {
let dp = this._testRunner._browserSession.protocol;
const sessionId = (await dp.Target.attachToTarget({targetId: this._targetId, flatten: true})).result.sessionId;
return new TestRunner.Session(this._testRunner, sessionId);
}
navigate(url) {
return this._navigate(this._testRunner.url(url));
}
async _navigate(url) {
var session = await this.createSession();
await session._navigate(url);
await session.disconnect();
}
async loadHTML(html) {
html = html.replace(/'/g, "\\'").replace(/\n/g, '\\n');
var session = await this.createSession();
await session.protocol.Runtime.evaluate({
awaitPromise: true,
expression: `
document.write('${html}');
// wait for all scripts to load
const promise = new Promise(x => window._loadHTMLResolve = x).then(() => {
delete window._loadHTMLResolve;
});
// We do a document.write here to serialize with the previous document.write
if (document.querySelector('script[src]'))
document.write('<script>window._loadHTMLResolve(); document.currentScript.remove();</script>');
else
window._loadHTMLResolve();
document.close();
promise;
`});
await session.disconnect();
}
};
TestRunner.Session = class {
constructor(testRunner, sessionId) {
this._testRunner = testRunner;
this._sessionId = sessionId;
this._requestId = 0;
this._eventHandlers = new Map();
this.protocol = this._setupProtocol();
this._parentSessionId = null;
DevToolsAPI._sessions.set(sessionId, this);
}
async disconnect() {
await DevToolsAPI._sendCommandOrDie(this._parentSessionId, 'Target.detachFromTarget', {sessionId: this._sessionId});
}
createChild(sessionId) {
const session = new TestRunner.Session(this._testRunner, sessionId);
session._parentSessionId = this._sessionId;
return session;
}
async sendCommand(method, params) {
var requestId = ++this._requestId;
if (this._testRunner._dumpInspectorProtocolMessages)
this._testRunner.log(`frontend => backend: ${JSON.stringify({method, params, sessionId: this._sessionId})}`);
const result = await DevToolsAPI._sendCommand(this._sessionId, method, params);
if (this._testRunner._dumpInspectorProtocolMessages)
this._testRunner.log(`backend => frontend: ${JSON.stringify(result)}`);
return result;
}
async evaluate(code, ...args) {
return this._innerEvaluate(false /* awaitPromise */, false /* userGesture */, code, ...args);
}
async evaluateAsync(code, ...args) {
return this._innerEvaluate(true /* awaitPromise */, false /* userGesture */, code, ...args);
}
async evaluateAsyncWithUserGesture(code, ...args) {
return this._innerEvaluate(true /* awaitPromise */, true /* userGesture */, code, ...args);
}
async _innerEvaluate(awaitPromise, userGesture, code, ...args) {
if (typeof code === 'function') {
var argsString = args.map(JSON.stringify.bind(JSON)).join(', ');
code = `(${code.toString()})(${argsString})`;
}
var response = await this.protocol.Runtime.evaluate({expression: code, returnByValue: true, awaitPromise, userGesture});
if (response.error) {
this._testRunner.log(`Error while evaluating async '${code}': ${response.error}`);
this._testRunner.completeTest();
} else {
return response.result.result.value;
}
}
navigate(url) {
return this._navigate(this._testRunner.url(url));
}
async _navigate(url) {
await this.protocol.Page.enable();
await this.protocol.Page.setLifecycleEventsEnabled({enabled: true});
const frameId = (await this.protocol.Page.navigate({url: url})).result.frameId;
await this.protocol.Page.onceLifecycleEvent(
event => event.params.name === 'load' && event.params.frameId === frameId);
}
_dispatchMessage(message) {
if (this._testRunner._dumpInspectorProtocolMessages)
this._testRunner.log(`backend => frontend: ${JSON.stringify(message)}`);
var eventName = message.method;
for (var handler of (this._eventHandlers.get(eventName) || []))
handler(message);
}
_setupProtocol() {
return new Proxy({}, {
get: (target, agentName, receiver) => new Proxy({}, {
get: (target, methodName, receiver) => {
const eventPattern = /^(on(ce)?|off)([A-Z][A-Za-z0-9]*)/;
var match = eventPattern.exec(methodName);
if (!match)
return args => this.sendCommand(
`${agentName}.${methodName}`, args || {});
var eventName = match[3];
eventName = eventName.charAt(0).toLowerCase() + eventName.slice(1);
if (match[1] === 'once')
return eventMatcher => this._waitForEvent(
`${agentName}.${eventName}`, eventMatcher);
if (match[1] === 'off')
return listener => this._removeEventHandler(
`${agentName}.${eventName}`, listener);
return listener => this._addEventHandler(
`${agentName}.${eventName}`, listener);
}
})
});
}
_addEventHandler(eventName, handler) {
var handlers = this._eventHandlers.get(eventName) || [];
handlers.push(handler);
this._eventHandlers.set(eventName, handlers);
}
_removeEventHandler(eventName, handler) {
var handlers = this._eventHandlers.get(eventName) || [];
var index = handlers.indexOf(handler);
if (index === -1)
return;
handlers.splice(index, 1);
this._eventHandlers.set(eventName, handlers);
}
_waitForEvent(eventName, eventMatcher) {
return new Promise(callback => {
var handler = result => {
if (eventMatcher && !eventMatcher(result))
return;
this._removeEventHandler(eventName, handler);
callback(result);
};
this._addEventHandler(eventName, handler);
});
}
};
var DevToolsAPI = {};
DevToolsAPI._requestId = 0;
DevToolsAPI._embedderMessageId = 0;
DevToolsAPI._dispatchTable = new Map();
DevToolsAPI._sessions = new Map();
DevToolsAPI._outputElement = null;
DevToolsAPI._log = function(text) {
if (!DevToolsAPI._outputElement) {
var intermediate = document.createElement('div');
document.body.appendChild(intermediate);
var intermediate2 = document.createElement('div');
intermediate.appendChild(intermediate2);
DevToolsAPI._outputElement = document.createElement('div');
DevToolsAPI._outputElement.className = 'output';
DevToolsAPI._outputElement.id = 'output';
DevToolsAPI._outputElement.style.whiteSpace = 'pre';
intermediate2.appendChild(DevToolsAPI._outputElement);
}
DevToolsAPI._outputElement.appendChild(document.createTextNode(text));
DevToolsAPI._outputElement.appendChild(document.createElement('br'));
};
DevToolsAPI._completeTest = function() {
testRunner.notifyDone();
};
DevToolsAPI._die = function(message, error) {
DevToolsAPI._log(`${message}: ${error}\n${error.stack}`);
DevToolsAPI._completeTest();
throw new Error();
};
DevToolsAPI.dispatchMessage = function(messageOrObject) {
var messageObject = (typeof messageOrObject === 'string' ? JSON.parse(messageOrObject) : messageOrObject);
var messageId = messageObject.id;
try {
if (typeof messageId === 'number') {
var handler = DevToolsAPI._dispatchTable.get(messageId);
if (handler) {
DevToolsAPI._dispatchTable.delete(messageId);
handler(messageObject);
} else {
DevToolsAPI._die(`Unexpected result id ${messageId}`);
}
} else {
var session = DevToolsAPI._sessions.get(messageObject.sessionId || '');
if (session)
session._dispatchMessage(messageObject);
}
} catch(e) {
DevToolsAPI._die(`Exception when dispatching message\n${JSON.stringify(messageObject)}`, e);
}
};
DevToolsAPI._sendCommand = function(sessionId, method, params) {
var requestId = ++DevToolsAPI._requestId;
var messageObject = {'id': requestId, 'method': method, 'params': params};
if (sessionId)
messageObject.sessionId = sessionId;
var embedderMessage = {'id': ++DevToolsAPI._embedderMessageId, 'method': 'dispatchProtocolMessage', 'params': [JSON.stringify(messageObject)]};
DevToolsHost.sendMessageToEmbedder(JSON.stringify(embedderMessage));
return new Promise(f => DevToolsAPI._dispatchTable.set(requestId, f));
};
DevToolsAPI._sendCommandOrDie = function(sessionId, method, params) {
return DevToolsAPI._sendCommand(sessionId, method, params).then(message => {
if (message.error)
DevToolsAPI._die('Error communicating with harness', new Error(JSON.stringify(message.error)));
return message.result;
});
};
DevToolsAPI._fetch = function(url) {
return new Promise(fulfill => {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = e => {
if (xhr.readyState !== XMLHttpRequest.DONE)
return;
if ([0, 200, 304].indexOf(xhr.status) === -1) // Testing harness file:/// results in 0.
DevToolsAPI._die(`${xhr.status} while fetching ${url}`, new Error());
else
fulfill(e.target.response);
};
xhr.send(null);
});
};
testRunner.dumpAsText();
testRunner.waitUntilDone();
testRunner.setCanOpenWindows(true);
window.addEventListener('load', () => {
var params = new URLSearchParams(window.location.search);
var testScriptURL = params.get('test');
var testBaseURL = testScriptURL.substring(0, testScriptURL.lastIndexOf('/') + 1);
var targetPageURL = params.get('target') || params.get('test');
var targetBaseURL = targetPageURL.substring(0, targetPageURL.lastIndexOf('/') + 1);
DevToolsAPI._fetch(testScriptURL).then(testScript => {
var testRunner = new TestRunner(testBaseURL, targetBaseURL, DevToolsAPI._log, DevToolsAPI._completeTest, DevToolsAPI._fetch, params);
var testFunction = eval(`${testScript}\n//# sourceURL=${testScriptURL}`);
if (params.get('debug')) {
var dispatch = DevToolsAPI.dispatchMessage;
var messages = [];
DevToolsAPI.dispatchMessage = message => {
if (!messages.length) {
setTimeout(() => {
for (var message of messages.splice(0))
dispatch(message);
}, 0);
}
messages.push(message);
};
testRunner.log = console.log;
testRunner.completeTest = () => console.log('Test completed');
window.test = () => testFunction(testRunner);
return;
}
return testFunction(testRunner);
}).catch(reason => {
DevToolsAPI._log(`Error while executing test script: ${reason}\n${reason.stack}`);
DevToolsAPI._completeTest();
});
}, false);
window['onerror'] = (message, source, lineno, colno, error) => {
DevToolsAPI._log(`${error}\n${error.stack}`);
DevToolsAPI._completeTest();
};
window.addEventListener('unhandledrejection', e => {
DevToolsAPI._log(`Promise rejection: ${e.reason}\n${e.reason ? e.reason.stack : ''}`);
DevToolsAPI._completeTest();
}, false);