зеркало из https://github.com/mozilla/gecko-dev.git
375 строки
11 KiB
JavaScript
375 строки
11 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "isXpcshell", function() {
|
|
let env = Cc["@mozilla.org/process/environment;1"].getService(
|
|
Ci.nsIEnvironment
|
|
);
|
|
return env.exists("XPCSHELL_TEST_PROFILE_DIR");
|
|
});
|
|
|
|
/**
|
|
* Checks whether the given error matches the given expectations.
|
|
*
|
|
* @param {*} error
|
|
* The error to check.
|
|
* @param {string|RegExp|function|null} expectedError
|
|
* The expectation to check against. If this parameter is:
|
|
*
|
|
* - a string, the error message must exactly equal the string.
|
|
* - a regular expression, it must match the error message.
|
|
* - a function, it is called with the error object and its
|
|
* return value is returned.
|
|
* @param {BaseContext} context
|
|
*
|
|
* @returns {boolean}
|
|
* True if the error matches the expected error.
|
|
*/
|
|
const errorMatches = (error, expectedError, context) => {
|
|
if (
|
|
typeof error === "object" &&
|
|
error !== null &&
|
|
!context.principal.subsumes(Cu.getObjectPrincipal(error))
|
|
) {
|
|
Cu.reportError("Error object belongs to the wrong scope.");
|
|
return false;
|
|
}
|
|
|
|
if (typeof expectedError === "function") {
|
|
return context.runSafeWithoutClone(expectedError, error);
|
|
}
|
|
|
|
if (
|
|
typeof error !== "object" ||
|
|
error == null ||
|
|
typeof error.message !== "string"
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (typeof expectedError === "string") {
|
|
return error.message === expectedError;
|
|
}
|
|
|
|
try {
|
|
return expectedError.test(error.message);
|
|
} catch (e) {
|
|
Cu.reportError(e);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// Checks whether |v| should use string serialization instead of JSON.
|
|
function useStringInsteadOfJSON(v) {
|
|
return (
|
|
// undefined to string, or else it is omitted from object after stringify.
|
|
v === undefined ||
|
|
// Values that would have become null.
|
|
(typeof v === "number" && (isNaN(v) || !isFinite(v)))
|
|
);
|
|
}
|
|
|
|
// A very strict deep equality comparator that throws for unsupported values.
|
|
// For context, see https://bugzilla.mozilla.org/show_bug.cgi?id=1782816#c2
|
|
function deepEquals(a, b) {
|
|
// Some values don't have a JSON representation. To disambiguate from null or
|
|
// regular strings, we prepend this prefix instead.
|
|
const NON_JSON_PREFIX = "#NOT_JSON_SERIALIZABLE#";
|
|
|
|
function replacer(key, value) {
|
|
if (typeof value == "object" && value !== null && !Array.isArray(value)) {
|
|
const cls = ChromeUtils.getClassName(value);
|
|
if (cls === "Object") {
|
|
// Return plain object with keys sorted in a predictable order.
|
|
return Object.fromEntries(
|
|
Object.keys(value)
|
|
.sort()
|
|
.map(k => [k, value[k]])
|
|
);
|
|
}
|
|
// Just throw to avoid potentially inaccurate serializations (e.g. {}).
|
|
throw new ExtensionUtils.ExtensionError(`Unsupported obj type: ${cls}`);
|
|
}
|
|
|
|
if (useStringInsteadOfJSON(value)) {
|
|
return `${NON_JSON_PREFIX}${value}`;
|
|
}
|
|
return value;
|
|
}
|
|
return JSON.stringify(a, replacer) === JSON.stringify(b, replacer);
|
|
}
|
|
|
|
/**
|
|
* Serializes the given value for use in informative assertion messages.
|
|
*
|
|
* @param {*} value
|
|
* @returns {string}
|
|
*/
|
|
const toSource = value => {
|
|
function cannotJSONserialize(v) {
|
|
return (
|
|
useStringInsteadOfJSON(v) ||
|
|
// Not a plain object. E.g. [object X], /regexp/, etc.
|
|
(typeof v == "object" &&
|
|
v !== null &&
|
|
!Array.isArray(v) &&
|
|
ChromeUtils.getClassName(v) !== "Object")
|
|
);
|
|
}
|
|
try {
|
|
if (cannotJSONserialize(value)) {
|
|
return String(value);
|
|
}
|
|
|
|
const replacer = (k, v) => (cannotJSONserialize(v) ? String(v) : v);
|
|
return JSON.stringify(value, replacer);
|
|
} catch (e) {
|
|
return "<unknown>";
|
|
}
|
|
};
|
|
|
|
this.test = class extends ExtensionAPI {
|
|
getAPI(context) {
|
|
const { extension } = context;
|
|
|
|
function getStack(savedFrame = null) {
|
|
if (savedFrame) {
|
|
return ChromeUtils.createError("", savedFrame).stack.replace(
|
|
/^/gm,
|
|
" "
|
|
);
|
|
}
|
|
return new context.Error().stack.replace(/^/gm, " ");
|
|
}
|
|
|
|
function assertTrue(value, msg) {
|
|
extension.emit(
|
|
"test-result",
|
|
Boolean(value),
|
|
String(msg),
|
|
getStack(context.getCaller())
|
|
);
|
|
}
|
|
|
|
class TestEventManager extends EventManager {
|
|
constructor(...args) {
|
|
super(...args);
|
|
|
|
// A map to keep track of the listeners wrappers being added in
|
|
// addListener (the wrapper will be needed to be able to remove
|
|
// the listener from this EventManager instance if the extension
|
|
// does call test.onMessage.removeListener).
|
|
this._listenerWrappers = new Map();
|
|
context.callOnClose({
|
|
close: () => this._listenerWrappers.clear(),
|
|
});
|
|
}
|
|
|
|
addListener(callback, ...args) {
|
|
const listenerWrapper = function(...args) {
|
|
try {
|
|
callback.call(this, ...args);
|
|
} catch (e) {
|
|
assertTrue(false, `${e}\n${e.stack}`);
|
|
}
|
|
};
|
|
super.addListener(listenerWrapper, ...args);
|
|
this._listenerWrappers.set(callback, listenerWrapper);
|
|
}
|
|
|
|
removeListener(callback) {
|
|
if (!this._listenerWrappers.has(callback)) {
|
|
return;
|
|
}
|
|
|
|
super.removeListener(this._listenerWrappers.get(callback));
|
|
this._listenerWrappers.delete(callback);
|
|
}
|
|
}
|
|
|
|
if (!Cu.isInAutomation && !isXpcshell) {
|
|
return { test: {} };
|
|
}
|
|
|
|
return {
|
|
test: {
|
|
withHandlingUserInput(callback) {
|
|
// TODO(Bug 1598804): remove this once we don't expose anymore the
|
|
// entire test API namespace based on an environment variable.
|
|
if (!Cu.isInAutomation) {
|
|
// This dangerous method should only be available if the
|
|
// automation pref is set, which is the case in browser tests.
|
|
throw new ExtensionUtils.ExtensionError(
|
|
"withHandlingUserInput can only be called in automation"
|
|
);
|
|
}
|
|
ExtensionCommon.withHandlingUserInput(
|
|
context.contentWindow,
|
|
callback
|
|
);
|
|
},
|
|
|
|
sendMessage(...args) {
|
|
extension.emit("test-message", ...args);
|
|
},
|
|
|
|
notifyPass(msg) {
|
|
extension.emit("test-done", true, msg, getStack(context.getCaller()));
|
|
},
|
|
|
|
notifyFail(msg) {
|
|
extension.emit(
|
|
"test-done",
|
|
false,
|
|
msg,
|
|
getStack(context.getCaller())
|
|
);
|
|
},
|
|
|
|
log(msg) {
|
|
extension.emit("test-log", true, msg, getStack(context.getCaller()));
|
|
},
|
|
|
|
fail(msg) {
|
|
assertTrue(false, msg);
|
|
},
|
|
|
|
succeed(msg) {
|
|
assertTrue(true, msg);
|
|
},
|
|
|
|
assertTrue(value, msg) {
|
|
assertTrue(value, msg);
|
|
},
|
|
|
|
assertFalse(value, msg) {
|
|
assertTrue(!value, msg);
|
|
},
|
|
|
|
assertDeepEq(expected, actual, msg) {
|
|
// The bindings generated by Schemas.jsm accepts any input, but the
|
|
// WebIDL-generated binding expects a structurally cloneable input.
|
|
// To ensure consistent behavior regardless of which mechanism was
|
|
// used, verify that the inputs are structurally cloneable.
|
|
// These will throw if the values cannot be cloned.
|
|
function ensureStructurallyCloneable(v) {
|
|
if (typeof v == "object" && v !== null) {
|
|
// Waive xrays to unhide callable members, so that cloneInto will
|
|
// throw if needed.
|
|
v = ChromeUtils.waiveXrays(v);
|
|
}
|
|
new StructuredCloneHolder(v, globalThis);
|
|
}
|
|
// When WebIDL bindings are used, the objects are already cloned
|
|
// structurally, so we don't need to check again.
|
|
if (!context.useWebIDLBindings) {
|
|
ensureStructurallyCloneable(expected);
|
|
ensureStructurallyCloneable(actual);
|
|
}
|
|
|
|
extension.emit(
|
|
"test-eq",
|
|
deepEquals(actual, expected),
|
|
String(msg),
|
|
toSource(expected),
|
|
toSource(actual),
|
|
getStack(context.getCaller())
|
|
);
|
|
},
|
|
|
|
assertEq(expected, actual, msg) {
|
|
let equal = expected === actual;
|
|
|
|
expected = String(expected);
|
|
actual = String(actual);
|
|
|
|
if (!equal && expected === actual) {
|
|
actual += " (different)";
|
|
}
|
|
extension.emit(
|
|
"test-eq",
|
|
equal,
|
|
String(msg),
|
|
expected,
|
|
actual,
|
|
getStack(context.getCaller())
|
|
);
|
|
},
|
|
|
|
assertRejects(promise, expectedError, msg) {
|
|
// Wrap in a native promise for consistency.
|
|
promise = Promise.resolve(promise);
|
|
|
|
return promise.then(
|
|
result => {
|
|
let message = `Promise resolved, expected rejection '${toSource(
|
|
expectedError
|
|
)}'`;
|
|
if (msg) {
|
|
message += `: ${msg}`;
|
|
}
|
|
assertTrue(false, message);
|
|
},
|
|
error => {
|
|
let expected = toSource(expectedError);
|
|
let message = `got '${toSource(error)}'`;
|
|
if (msg) {
|
|
message += `: ${msg}`;
|
|
}
|
|
|
|
assertTrue(
|
|
errorMatches(error, expectedError, context),
|
|
`Promise rejected, expecting rejection to match '${expected}', ${message}`
|
|
);
|
|
}
|
|
);
|
|
},
|
|
|
|
assertThrows(func, expectedError, msg) {
|
|
try {
|
|
func();
|
|
|
|
let message = `Function did not throw, expected error '${toSource(
|
|
expectedError
|
|
)}'`;
|
|
if (msg) {
|
|
message += `: ${msg}`;
|
|
}
|
|
assertTrue(false, message);
|
|
} catch (error) {
|
|
let expected = toSource(expectedError);
|
|
let message = `got '${toSource(error)}'`;
|
|
if (msg) {
|
|
message += `: ${msg}`;
|
|
}
|
|
|
|
assertTrue(
|
|
errorMatches(error, expectedError, context),
|
|
`Function threw, expecting error to match '${expected}', ${message}`
|
|
);
|
|
}
|
|
},
|
|
|
|
onMessage: new TestEventManager({
|
|
context,
|
|
name: "test.onMessage",
|
|
register: fire => {
|
|
let handler = (event, ...args) => {
|
|
fire.async(...args);
|
|
};
|
|
|
|
extension.on("test-harness-message", handler);
|
|
return () => {
|
|
extension.off("test-harness-message", handler);
|
|
};
|
|
},
|
|
}).api(),
|
|
},
|
|
};
|
|
}
|
|
};
|