286 lines
9.7 KiB
JavaScript
286 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(actualValues) {
|
|
const isUpdate = Services.env.get(UPDATE_SNAPSHOT_ENV) == "true";
|
|
if (!isUpdate) {
|
|
return;
|
|
}
|
|
if (!actualValues?.length) {
|
|
return;
|
|
}
|
|
|
|
const filePath = gExpectedValuesFilePath;
|
|
const assertionValues = [];
|
|
let i = 0;
|
|
|
|
for (const objectDescription of AllObjects) {
|
|
if (objectDescription.disabled) {
|
|
continue;
|
|
}
|
|
|
|
if (i >= actualValues.length) {
|
|
throw new Error("Unexpected discrepencies between the reported evaled strings and expected values");
|
|
}
|
|
|
|
const value = actualValues[i++]
|
|
|
|
// 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 (value == undefined) {
|
|
continue;
|
|
}
|
|
|
|
let evaled = objectDescription.expression;
|
|
// 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" +
|
|
// Ident each newline to match the array item indentation
|
|
JSON.stringify(value, null, 2).replace(/^/gm, " ") +
|
|
","
|
|
);
|
|
}
|
|
|
|
if (i != actualValues.length) {
|
|
throw new Error("Unexpected discrepencies between the reported evaled strings and expected values");
|
|
}
|
|
|
|
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 testOrUpdateExpectedValues(expectedValuesFileName, actualValues) {
|
|
const isMochitest = "gTestPath" in gTestScope;
|
|
|
|
let expectedValues = loadExpectedValues(expectedValuesFileName);
|
|
if (expectedValues) {
|
|
// Clone the Array as we are going to mutate it via Array.shift().
|
|
expectedValues = [...expectedValues];
|
|
|
|
const testPath = isMochitest ? gTestScope.gTestPath.replace("chrome://mochitest/content/browser/", "") : "path/to/your/xpcshell/test";
|
|
const failureWarningMessage = "DEVTOOLS GENERATED-TEST FAILURE! SEE: https://firefox-source-docs.mozilla.org/devtools/tests/js-object-tests.html\n" +
|
|
"This test uses a generated file which might need to be updated by running:\n" +
|
|
` $ mach test ${testPath} --headless --setenv ${UPDATE_SNAPSHOT_ENV}=true`;
|
|
|
|
let failed = false;
|
|
let i = 0;
|
|
for (const objectDescription of AllObjects) {
|
|
if (objectDescription.disabled) {
|
|
continue;
|
|
}
|
|
const actual = actualValues[i++];
|
|
const expression = objectDescription.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;
|
|
}
|
|
|
|
let failureMessage = `Got expected output for "${expression}"`;
|
|
const expected = expectedValues.shift();
|
|
if (!ObjectUtils.deepEqual(actual, expected)) {
|
|
if (isMochitest) {
|
|
if (!failed) {
|
|
// If this is the first failure, log a first failure with the full
|
|
// failure message including a link to the documentation.
|
|
failed = true;
|
|
gTestScope.Assert.ok(false, failureWarningMessage);
|
|
}
|
|
gTestScope.Assert.deepEqual(actual, expected, failureMessage);
|
|
} else {
|
|
// For xpcshell tests, the first failure will throw and stop the test
|
|
// so include both the documentation link and the failure details.
|
|
failureMessage = failureWarningMessage + "\n\n" + failureMessage;
|
|
gTestScope.Assert.deepEqual(actual, expected, failureMessage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mayBeSaveExpectedValues(actualValues);
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
const actualValues = [];
|
|
|
|
if (!gTestScope) {
|
|
throw new Error("`JSObjectsTestUtils.init()` should be called before `runTest()`");
|
|
}
|
|
|
|
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 });
|
|
actualValues.push(actual);
|
|
}
|
|
|
|
testOrUpdateExpectedValues(expectedValuesFileName, actualValues);
|
|
}
|
|
|
|
export const JSObjectsTestUtils = { init, runTest, testOrUpdateExpectedValues };
|