268 lines
9.7 KiB
JavaScript
268 lines
9.7 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
* http://creativecommons.org/publicdomain/zero/1.0/
|
|
*/
|
|
|
|
import { ObjectUtils } from "resource://gre/modules/ObjectUtils.sys.mjs";
|
|
import { TEST_PAGE_HTML, CONTEXTS, AllObjects } from "resource://testing-common/AllJavascriptTypes.mjs";
|
|
import { AddonTestUtils } from "resource://testing-common/AddonTestUtils.sys.mjs"
|
|
|
|
// Name of the environment variable to set while running the test to update the expected values
|
|
const UPDATE_SNAPSHOT_ENV = "UPDATE_SNAPSHOT";
|
|
|
|
export { CONTEXTS } from "resource://testing-common/AllJavascriptTypes.mjs";
|
|
|
|
// To avoid totally unrelated exceptions about missing appinfo when running from xpcshell tests
|
|
const isXpcshell = Services.env.exists("XPCSHELL_TEST_PROFILE_DIR");
|
|
if (isXpcshell) {
|
|
AddonTestUtils.createAppInfo(
|
|
"xpcshell@tests.mozilla.org",
|
|
"XPCShell",
|
|
"42",
|
|
"42"
|
|
);
|
|
}
|
|
|
|
let gTestScope;
|
|
|
|
/**
|
|
* Initialize the test helper.
|
|
*
|
|
* @param {Object} testScope
|
|
* XPCShell or mochitest test scope (i.e. the global object of the test currently executed)
|
|
*/
|
|
function init(testScope) {
|
|
if (!testScope?.gTestPath && !testScope?.Assert) {
|
|
throw new Error("`JSObjectsTestUtils.init()` should be called with the (xpcshell or mochitest) test global object");
|
|
}
|
|
gTestScope = testScope;
|
|
|
|
if ("gTestPath" in testScope) {
|
|
AddonTestUtils.initMochitest(testScope);
|
|
} else {
|
|
AddonTestUtils.init(testScope);
|
|
}
|
|
|
|
const server = AddonTestUtils.createHttpServer({
|
|
hosts: ["example.com"],
|
|
});
|
|
|
|
server.registerPathHandler("/", (request, response) => {
|
|
response.setHeader("Content-Type", "text/html");
|
|
response.write(TEST_PAGE_HTML);
|
|
});
|
|
|
|
// Lookup for all preferences to toggle in order to have all the expected objects type functional
|
|
const prefValues = new Map();
|
|
for (const { prefs } of AllObjects) {
|
|
if (!prefs) {
|
|
continue;
|
|
}
|
|
for (const elt of prefs) {
|
|
if (elt.length != 2) {
|
|
throw new Error("Each pref should be an array of two element [prefName, prefValue]. Got: "+elt);
|
|
}
|
|
const [ name, value ] = elt;
|
|
const otherValue = prefValues.get(name);
|
|
if (otherValue && otherValue != value) {
|
|
throw new Error(`Two javascript values in AllJavascriptTypes.mjs are expecting different values for '${name}' preference. (${otherValue} vs ${value})`);
|
|
}
|
|
prefValues.set(name, value);
|
|
if (typeof(value) == "boolean") {
|
|
Services.prefs.setBoolPref(name, value);
|
|
gTestScope.registerCleanupFunction(() => {
|
|
Services.prefs.clearUserPref(name);
|
|
});
|
|
} else {
|
|
throw new Error("Unsupported pref type: "+name+" = "+value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let gExpectedValuesFilePath;
|
|
let gCurrentTestFolderUrl;
|
|
|
|
const chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
|
|
Ci.nsIChromeRegistry
|
|
);
|
|
function loadExpectedValues(expectedValuesFileName) {
|
|
const isUpdate = Services.env.get(UPDATE_SNAPSHOT_ENV) == "true";
|
|
dump(`JS Objects test: ${isUpdate ? "Update" : "Check"} ${expectedValuesFileName}\n`);
|
|
|
|
// Depending on the test suite, mochitest will expose `gTextPath` which is a chrome://
|
|
// for the current test file.
|
|
// Otherwise xpcshell will expose `resource://test/` for the current test folder.
|
|
gCurrentTestFolderUrl = "gTestPath" in gTestScope
|
|
? gTestScope.gTestPath.substr(0, gTestScope.gTestPath.lastIndexOf("/")) + "/"
|
|
: "resource://test/";
|
|
|
|
// Build the URL for the test data file
|
|
const url = gCurrentTestFolderUrl + expectedValuesFileName;
|
|
|
|
// Resolve the test data file URL into a file absolute path
|
|
if (url.startsWith("chrome")) {
|
|
const chromeURL = Services.io.newURI(url);
|
|
gExpectedValuesFilePath = chromeRegistry
|
|
.convertChromeURL(chromeURL)
|
|
.QueryInterface(Ci.nsIFileURL).file.path;
|
|
} else if (url.startsWith("resource")) {
|
|
const resURL = Services.io.newURI(url);
|
|
const resHandler = Services.io.getProtocolHandler("resource")
|
|
.QueryInterface(Ci.nsIResProtocolHandler);
|
|
gExpectedValuesFilePath = Services.io.newURI(resHandler.resolveURI(resURL)).QueryInterface(Ci.nsIFileURL).file.path;
|
|
}
|
|
|
|
if (!isUpdate) {
|
|
dump(`Loading test data file: ${url}\n`);
|
|
return ChromeUtils.importESModule(url).default;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async function mayBeSaveExpectedValues(evaledStrings, newExpectedValues) {
|
|
if (!newExpectedValues?.length) {
|
|
return;
|
|
}
|
|
|
|
if (evaledStrings.length != newExpectedValues.length) {
|
|
throw new Error("Unexpected discrepencies between the reported evaled strings and expected values");
|
|
}
|
|
|
|
const filePath = gExpectedValuesFilePath;
|
|
const assertionValues = [];
|
|
let i = 0;
|
|
for (const value of newExpectedValues) {
|
|
let evaled = evaledStrings[i];
|
|
// Remove any first empty line
|
|
evaled = evaled.replace(/^\s*\n/, "");
|
|
// remove the unnecessary indentation
|
|
const m = evaled.match(/^( +)/);
|
|
if (m && m[1]) {
|
|
const regexp = new RegExp("^"+m[1], "gm");
|
|
evaled = evaled.replace(regexp, "");
|
|
}
|
|
// Ensure prefixing all new lines in the evaled string with " //"
|
|
// to keep it being in a code comment.
|
|
evaled = evaled.replace(/\r?\n/g, "\n // ");
|
|
|
|
assertionValues.push(
|
|
" // " + evaled +
|
|
"\n" +
|
|
" " +
|
|
JSON.stringify(value, null, 2) +
|
|
","
|
|
);
|
|
i++;
|
|
}
|
|
const fileContent = `/* Any copyright is dedicated to the Public Domain.
|
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
/*
|
|
* THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND.
|
|
*
|
|
* More info in https://firefox-source-docs.mozilla.org/devtools/tests/js-object-tests.html
|
|
*/
|
|
|
|
export default [
|
|
${assertionValues.join("\n\n")}
|
|
];`;
|
|
dump("Writing: " + fileContent + " in " + filePath + "\n");
|
|
await IOUtils.write(filePath, new TextEncoder().encode(fileContent));
|
|
}
|
|
|
|
async function runTest(expectedValuesFileName, testFunction) {
|
|
if (!gTestScope) {
|
|
throw new Error("`JSObjectsTestUtils.init()` should be called before `runTest()`");
|
|
}
|
|
if (typeof (expectedValuesFileName) != "string") {
|
|
throw new Error("`JSObjectsTestUtils.runTest()` first argument should be a data file name");
|
|
}
|
|
if (typeof (testFunction) != "function") {
|
|
throw new Error("`JSObjectsTestUtils.runTest()` second argument should be a test function");
|
|
}
|
|
|
|
|
|
let expectedValues = loadExpectedValues(expectedValuesFileName);
|
|
if (expectedValues) {
|
|
// Clone the Array as we are going to mutate it via Array.shift().
|
|
expectedValues = [...expectedValues];
|
|
}
|
|
|
|
const evaledStrings = [];
|
|
const newExpectedValues = [];
|
|
|
|
let failed = false;
|
|
|
|
for (const objectDescription of AllObjects) {
|
|
if (objectDescription.disabled) {
|
|
continue;
|
|
}
|
|
|
|
const { context, expression } = objectDescription;
|
|
if (!Object.values(CONTEXTS).includes(context)) {
|
|
throw new Error("Missing, or invalid context in: " + JSON.stringify(objectDescription));
|
|
}
|
|
|
|
if (!expression) {
|
|
throw new Error("Missing a value in: " + JSON.stringify(objectDescription));
|
|
}
|
|
|
|
const actual = await testFunction({ context, expression });
|
|
|
|
// Ignore this JS object as the test function did not return any actual value.
|
|
// We assume none of the tests would store "undefined" as a target value.
|
|
if (actual == undefined) {
|
|
continue;
|
|
}
|
|
|
|
const testPath = "gtestPath" in gTestScope ? gTestScope.gTestPath.replace("chrome://mochitest/content/browser/", "") : "path/to/your/xpcshell/test";
|
|
const failureMessage = `This is a JavaScript value processing test, which includes an automatically generated snapshot file (${expectedValuesFileName}).\n` +
|
|
"You may update this file by running:`\n" +
|
|
` $ mach test ${testPath} --headless --setenv ${UPDATE_SNAPSHOT_ENV}=true\n` +
|
|
"And then carefuly review if the result is valid regarding your ongoing changes.\n" +
|
|
"`More info in https://firefox-source-docs.mozilla.org/devtools/tests/js-object-tests.html\n";
|
|
|
|
const isMochitest = "gTestPath" in gTestScope;
|
|
|
|
// If we aren't in "update" mode, we are reading assertion values from $EXPECTED_VALUES_FILE
|
|
// and will assert the current returned values against these values
|
|
if (expectedValues) {
|
|
const expected = expectedValues.shift();
|
|
try {
|
|
gTestScope.Assert.deepEqual(actual, expected, `Got expected output for "${expression}"`);
|
|
} catch(e) {
|
|
// deepEqual only throws in case of differences when running in XPCShell tests. Mochitest won't throw and keep running.
|
|
// XPCShell will stop at the first failing assertion, so ensure showing our failure message and ok() will throw and stop the test.
|
|
if (!isMochitest) {
|
|
gTestScope.Assert.ok(false, failureMessage);
|
|
}
|
|
throw e;
|
|
}
|
|
// As mochitest won't throw when calling deepEqual with differences in the objects,
|
|
// we have to recompute the difference in order to know if any of the tests failed.
|
|
if (isMochitest && !failed && !ObjectUtils.deepEqual(actual, expected)) {
|
|
failed = true;
|
|
}
|
|
} else {
|
|
// Otherwise, if we are in update mode, we will collected all current values
|
|
// in order to store them in $EXPECTED_VALUES_FILE
|
|
//
|
|
// Force casting to string, in case this is a function.
|
|
evaledStrings.push(String(expression));
|
|
newExpectedValues.push(actual);
|
|
}
|
|
}
|
|
|
|
if (failed) {
|
|
const failureMessage = "This is a JavaScript value processing test, which includes an automatically generated snapshot file.\n" +
|
|
"If the change made to that snapshot file makes sense, you may simply update them by running:`\n" +
|
|
" $ mach test ${testPath} --headless --setenv UPDATE_EXPECTED_VALUES=true\n" +
|
|
"`More info in devtools/shared/tests/objects/README.md\n";
|
|
gTestScope.Assert.ok(false, failureMessage);
|
|
}
|
|
|
|
mayBeSaveExpectedValues(evaledStrings, newExpectedValues);
|
|
}
|
|
|
|
export const JSObjectsTestUtils = { init, runTest };
|