blob: 6239c8c201e8e1a449615d4e6fdd973d8996cc05 [file] [log] [blame]
/*
* Copyright (C) 2012 Samsung Electronics. All rights reserved.
* Copyright (C) 2014, 2015 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
InspectorFrontendAPI = {};
InjectedTestHarness = class InjectedTestHarness
{
constructor()
{
this._logCount = 0;
this.forceSyncDebugLogging = false;
}
completeTest()
{
throw new Error("Must be implemented by subclasses.");
}
addResult()
{
throw new Error("Must be implemented by subclasses.");
}
debugLog()
{
throw new Error("Must be implemented by subclasses.");
}
evaluateInPage(string, callback)
{
throw new Error("Must be implemented by subclasses.");
}
createAsyncSuite(name)
{
return new InjectedTestHarness.AsyncTestSuite(this, name);
}
createSyncSuite(name)
{
return new InjectedTestHarness.SyncTestSuite(this, name);
}
get logCount()
{
return this._logCount;
}
log(message)
{
++this._logCount;
if (this.forceSyncDebugLogging)
this.debugLog(message);
else
this.addResult(message);
}
assert(condition, message)
{
if (condition)
return;
let stringifiedMessage = typeof message !== "object" ? message : JSON.stringify(message);
this.addResult("ASSERT: " + stringifiedMessage);
}
expectThat(condition, message)
{
let prefix = condition ? "PASS" : "FAIL";
let stringifiedMessage = typeof message !== "object" ? message : JSON.stringify(message);
this.addResult(prefix + ": " + stringifiedMessage);
}
}
InjectedTestHarness.AsyncTestSuite = class AsyncTestSuite
{
constructor(harness, name) {
if (!(harness instanceof InjectedTestHarness))
throw new Error("Must pass the test's harness as the first argument.");
if (!name || typeof name !== "string")
throw new Error("Tried to create AsyncTestSuite without string suite name.");
this.name = name;
this._harness = harness;
this.testcases = [];
this.runCount = 0;
this.failCount = 0;
}
get passCount()
{
return this.runCount - this.failCount;
}
get skipCount()
{
if (this.failCount)
return this.testcases.length - this.runCount;
else
return 0;
}
addTestCase(testcase)
{
if (!testcase || !(testcase instanceof Object))
throw new Error("Tried to add non-object test case.");
if (typeof testcase.name !== "string")
throw new Error("Tried to add test case without a name.");
if (typeof testcase.test !== "function")
throw new Error("Tried to add test case without `test` function.");
this.testcases.push(testcase);
}
// Use this if the test file only has one suite, and no handling
// of the promise returned by runTestCases() is needed.
runTestCasesAndFinish()
{
function finish() {
this._harness.completeTest();
}
this.runTestCases()
.then(finish.bind(this))
.catch(finish.bind(this));
}
runTestCases()
{
if (!this.testcases.length)
throw new Error("Tried to call runTestCases() for suite with no test cases");
if (this._startedRunning)
throw new Error("Tried to call runTestCases() more than once.");
this._startedRunning = true;
this._harness.log("");
this._harness.log("== Running test suite: " + this.name);
// Avoid adding newlines if nothing was logged.
var priorLogCount = this._harness.logCount;
var suite = this;
var result = this.testcases.reduce(function(chain, testcase, i) {
return chain.then(function() {
if (i > 0 && priorLogCount + 1 < suite._harness.logCount)
suite._harness.log("");
priorLogCount = suite._harness.logCount;
suite._harness.log("-- Running test case: " + testcase.name);
suite.runCount++;
return new Promise(testcase.test);
});
}, Promise.resolve());
return result.catch(function(e) {
suite.failCount++;
var message = e;
if (e instanceof Error)
message = e.message;
if (typeof message !== "string")
message = JSON.stringify(message);
suite._harness.log("!! EXCEPTION: " + message);
throw e; // Reject this promise by re-throwing the error.
});
}
}
InjectedTestHarness.SyncTestSuite = class SyncTestSuite
{
constructor(harness, name) {
if (!(harness instanceof InjectedTestHarness))
throw new Error("Must pass the test's harness as the first argument.");
if (!name || typeof name !== "string")
throw new Error("Tried to create SyncTestSuite without string suite name.");
this.name = name;
this._harness = harness;
this.testcases = [];
this.runCount = 0;
this.failCount = 0;
}
get passCount()
{
return this.runCount - this.failCount;
}
get skipCount()
{
if (this.failCount)
return this.testcases.length - this.runCount;
else
return 0;
}
addTestCase(testcase)
{
if (!testcase || !(testcase instanceof Object))
throw new Error("Tried to add non-object test case.");
if (typeof testcase.name !== "string")
throw new Error("Tried to add test case without a name.");
if (typeof testcase.test !== "function")
throw new Error("Tried to add test case without `test` function.");
this.testcases.push(testcase);
}
// Use this if the test file only has one suite.
runTestCasesAndFinish()
{
this.runTestCases();
this._harness.completeTest();
}
runTestCases()
{
if (!this.testcases.length)
throw new Error("Tried to call runTestCases() for suite with no test cases");
if (this._startedRunning)
throw new Error("Tried to call runTestCases() more than once.");
this._startedRunning = true;
this._harness.log("");
this._harness.log("== Running test suite: " + this.name);
var priorLogCount = this._harness.logCount;
var suite = this;
for (var i = 0; i < this.testcases.length; i++) {
var testcase = this.testcases[i];
if (i > 0 && priorLogCount + 1 < this._harness.logCount)
this._harness.log("");
priorLogCount = this._harness.logCount;
this._harness.log("-- Running test case: " + testcase.name);
suite.runCount++;
try {
var result = testcase.test.call(null);
if (result === false) {
suite.failCount++;
return false;
}
} catch (e) {
suite.failCount++;
var message = e;
if (e instanceof Error)
message = e.message;
else
e = new Error(e);
if (typeof message !== "string")
message = JSON.stringify(message);
this._harness.log("!! EXCEPTION: " + message);
return false;
}
}
return true;
}
}
class ProtocolTestHarness extends InjectedTestHarness
{
// InjectedTestHarness Overrides
completeTest()
{
this.evaluateInPage("closeTest();");
}
addResult(message)
{
// Unfortunately, every string argument must be escaped because tests are not consistent
// with respect to escaping with single or double quotes. Some exceptions use single quotes.
var stringifiedMessage = typeof message !== "string" ? JSON.stringify(message) : message;
this.evaluateInPage("log(unescape('" + escape(stringifiedMessage) + "'));");
}
debugLog(message)
{
var stringifiedMessage = typeof message !== "string" ? JSON.stringify(message) : message;
this.evaluateInPage("debugLog(unescape('" + escape(stringifiedMessage) + "'));")
}
evaluateInPage(expression, callback)
{
let args = {
method: "Runtime.evaluate",
params: {expression}
}
if (typeof callback === "function")
InspectorProtocol.sendCommand(args, callback);
else
return InspectorProtocol.awaitCommand(args);
}
}
window.ProtocolTest = new ProtocolTestHarness();
InspectorProtocol = {};
InspectorProtocol._dispatchTable = [];
InspectorProtocol._requestId = -1;
InspectorProtocol.eventHandler = {};
InspectorProtocol.dumpInspectorProtocolMessages = false;
InspectorProtocol.sendCommand = function(methodOrObject, params, handler)
{
// Allow new-style arguments object, as in awaitCommand.
var method = methodOrObject;
if (typeof methodOrObject === "object")
var {method, params, handler} = methodOrObject;
this._dispatchTable[++this._requestId] = handler;
var messageObject = {method, params, "id": this._requestId};
this.sendMessage(messageObject);
return this._requestId;
}
InspectorProtocol.awaitCommand = function(args)
{
var {method, params} = args;
return new Promise(function(resolve, reject) {
this._dispatchTable[++this._requestId] = {resolve, reject};
var messageObject = {method, params, "id": this._requestId};
this.sendMessage(messageObject);
}.bind(this));
}
InspectorProtocol.awaitEvent = function(args)
{
var {event} = args;
if (typeof event !== "string")
throw new Error("Event must be a string.");
return new Promise(function(resolve, reject) {
InspectorProtocol.eventHandler[event] = function(message) {
InspectorProtocol.eventHandler[event] = undefined;
resolve(message);
}
});
}
InspectorProtocol.addEventListener = function(eventTypeOrObject, listener)
{
var event = eventTypeOrObject;
if (typeof eventTypeOrObject === "object")
var {event, listener} = eventTypeOrObject;
if (typeof event !== "string")
throw new Error("Event name must be a string.");
if (typeof listener !== "function")
throw new Error("Event listener must be callable.");
// Convert to an array of listeners.
var listeners = InspectorProtocol.eventHandler[event];
if (!listeners)
listeners = InspectorProtocol.eventHandler[event] = [];
else if (typeof listeners === "function")
listeners = InspectorProtocol.eventHandler[event] = [listeners];
// Prevent registering multiple times.
if (listeners.includes(listener))
throw new Error("Cannot register the same listener more than once.");
listeners.push(listener);
}
InspectorProtocol.sendMessage = function(messageObject)
{
// This matches the debug dumping in InspectorBackend, which is bypassed
// by InspectorProtocol. Return messages should be dumped by InspectorBackend.
if (InspectorProtocol.dumpInspectorProtocolMessages)
console.log("frontend: " + JSON.stringify(messageObject));
InspectorFrontendHost.sendMessageToBackend(JSON.stringify(messageObject));
}
InspectorProtocol.checkForError = function(responseObject)
{
if (responseObject.error) {
ProtocolTest.log("PROTOCOL ERROR: " + JSON.stringify(responseObject.error));
ProtocolTest.completeTest();
throw "PROTOCOL ERROR";
}
}
InspectorFrontendAPI.dispatchMessageAsync = function(messageObject)
{
// This matches the debug dumping in InspectorBackend, which is bypassed
// by InspectorProtocol. Return messages should be dumped by InspectorBackend.
if (InspectorProtocol.dumpInspectorProtocolMessages)
console.log("backend: " + JSON.stringify(messageObject));
// If the message has an id, then it is a reply to a command.
var messageId = messageObject["id"];
if (typeof messageId === "number") {
var handler = InspectorProtocol._dispatchTable[messageId];
if (!handler)
return;
if (typeof handler === "function")
handler(messageObject);
else if (typeof handler === "object") {
var {resolve, reject} = handler;
if ("error" in messageObject)
reject(messageObject.error.message);
else
resolve(messageObject.result);
}
// Otherwise, it is an event.
} else {
var eventName = messageObject["method"];
var handler = InspectorProtocol.eventHandler[eventName];
if (!handler)
return;
if (typeof handler === "function")
handler(messageObject);
else if (handler instanceof Array) {
handler.map(function(listener) {
listener.call(null, messageObject);
});
} else if (typeof handler === "object") {
var {resolve, reject} = handler;
if ("error" in messageObject)
reject(messageObject.error.message);
else
resolve(messageObject.result);
}
}
}
window.addEventListener("message", function(event) {
try {
eval(event.data);
} catch (e) {
alert(e.stack);
ProtocolTest.completeTest();
throw e;
}
});