Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test gets skipped with pattern: !fission
- Manifest: devtools/client/shared/components/test/browser/browser.toml
/* Any copyright is dedicated to the Public Domain.
"use strict";
Services.scriptloader.loadSubScript(
this
);
const TEST_URI = "data:text/html;charset=utf-8,stub generation";
/**
* A Map keyed by filename, and for which the value is also a Map, with the key being the
* label for the stub, and the value the expression to evaluate to get the stub.
*/
const EXPRESSIONS_BY_FILE = {
"attribute.js": new Map([
[
"Attribute",
`{
const a = document.createAttribute("class")
a.value = "autocomplete-suggestions";
a;
}`,
],
]),
"comment-node.js": new Map([
[
"Comment",
`{
document.createComment("test\\nand test\\nand test\\nand test\\nand test\\nand test\\nand test")
}`,
],
]),
"date-time.js": new Map([
["DateTime", `new Date(1459372644859)`],
["InvalidDateTime", `new Date("invalid")`],
]),
"infinity.js": new Map([
["Infinity", `Infinity`],
["NegativeInfinity", `-Infinity`],
]),
"nan.js": new Map([["NaN", `2 * document`]]),
"null.js": new Map([["Null", `null`]]),
"number.js": new Map([
["Int", `2 + 3`],
["True", `true`],
["False", `false`],
["NegZeroGrip", `1 / -Infinity`],
]),
"stylesheet.js": new Map([
[
"StyleSheet",
{
expression: `
(async function() {
const link = document.createElement("link");
link.setAttribute("rel", "stylesheet");
link.type = "text/css";
const onStylesheetHandled = new Promise(res => {
// The file does not exist so we'll get an error event, but it will
// still be put in document.styleSheets with its src, which is what we want.
link.addEventListener("error", () => res(), { once: true});
})
document.head.appendChild(link);
await onStylesheetHandled;
return document.styleSheets[0];
})()
`,
async: true,
},
],
]),
"symbol.js": new Map([
["Symbol", `Symbol("foo")`],
["SymbolWithoutIdentifier", `Symbol()`],
["SymbolWithLongString", `Symbol("aa".repeat(10000))`],
]),
"text-node.js": new Map([
[
"testRendering",
`let tn = document.createTextNode("hello world");
document.body.append(tn);
tn;`,
],
["testRenderingDisconnected", `document.createTextNode("hello world")`],
["testRenderingWithEOL", `document.createTextNode("hello\\nworld")`],
["testRenderingWithDoubleQuote", `document.createTextNode('hello"world')`],
[
"testRenderingWithLongString",
`document.createTextNode("a\\n" + ("a").repeat(20000))`,
],
]),
"undefined.js": new Map([["Undefined", `undefined`]]),
"window.js": new Map([
["Window", `window`],
[
"CrossOriginIframeContentWindow",
{
expression: `
(async function() {
const iframe = document.createElement("iframe");
const onLoaded = new Promise(resolve =>
iframe.addEventListener("load", resolve, {once: true})
);
document.body.append(iframe);
await onLoaded;
return iframe.contentWindow;
})()
`,
async: true,
},
],
[
"CrossOriginIframeTopWindow",
{
expression: `window.top`,
iframeUrlForExecution:
},
],
]),
// the following file.
// "accessible.js",
// "accessor.js",
// "big-int.js",
// "document-type.js",
// "document.js",
// "element-node.js",
// "error.js",
// "event.js",
// "failure.js",
// "function.js",
// "grip-array.js",
// "grip-entry.js",
// "grip-map.js",
// "grip.js",
// "long-string.js",
// "object-with-text.js",
// "object-with-url.js",
// "promise.js",
// "regexp.js",
};
add_task(async function () {
const isStubsUpdate = Services.env.get(STUBS_UPDATE_ENV) == "true";
const tab = await addTab(TEST_URI);
const {
CommandsFactory,
} = require("devtools/shared/commands/commands-factory");
const commands = await CommandsFactory.forTab(tab);
await commands.targetCommand.startListening();
let failed = false;
for (const stubFile of Object.keys(EXPRESSIONS_BY_FILE)) {
info(`${isStubsUpdate ? "Update" : "Check"} ${stubFile}`);
const generatedStubs = await generateStubs(commands, stubFile);
if (isStubsUpdate) {
await writeStubsToFile(stubFile, generatedStubs);
ok(true, `${stubFile} was updated`);
continue;
}
const existingStubs = getStubFile(stubFile);
if (generatedStubs.size !== existingStubs.size) {
failed = true;
continue;
}
for (const [key, packet] of generatedStubs) {
const packetStr = getSerializedPacket(packet, {
sortKeys: true,
replaceActorIds: true,
});
const grip = getSerializedPacket(existingStubs.get(key), {
sortKeys: true,
replaceActorIds: true,
});
is(packetStr, grip, `"${key}" packet has expected value`);
failed = failed || packetStr !== grip;
}
}
if (failed) {
ok(
false,
"The reps stubs need to be updated by running `" +
`mach test ${getCurrentTestFilePath()} --headless --setenv STUBS_UPDATE=true` +
"`"
);
} else {
ok(true, "Stubs are up to date");
}
await removeTab(tab);
});
async function generateStubs(commands, stubFile) {
const stubs = new Map();
for (const [key, options] of EXPRESSIONS_BY_FILE[stubFile]) {
const expression =
typeof options == "string" ? options : options.expression;
const executeOptions = {};
if (options.async === true) {
executeOptions.mapped = { await: true };
}
if (options.iframeUrlForExecution) {
const { promise: onIframeTargetCreated, resolve } =
Promise.withResolvers();
const onTargetAvailable = ({ targetFront }) => {
if (targetFront.url === options.iframeUrlForExecution) {
resolve(targetFront);
}
};
await commands.targetCommand.watchTargets({
types: [commands.targetCommand.TYPES.FRAME],
onAvailable: onTargetAvailable,
});
await SpecialPowers.spawn(
gBrowser.selectedBrowser,
[options.iframeUrlForExecution],
url => {
const iframe = content.document.createElement("iframe");
iframe.src = url;
content.document.body.append(iframe);
}
);
const targetFront = await onIframeTargetCreated;
executeOptions.selectedTargetFront = targetFront;
await commands.targetCommand.unwatchTargets({
types: [commands.targetCommand.TYPES.FRAME],
onAvailable: onTargetAvailable,
});
}
const { result } = await commands.scriptCommand.execute(
expression,
executeOptions
);
stubs.set(key, getCleanedPacket(stubFile, key, result));
}
return stubs;
}
function getCleanedPacket(stubFile, key, packet) {
// Remove the targetFront property that has a cyclical reference and that we don't need
// in our node tests.
delete packet.targetFront;
const existingStubs = getStubFile(stubFile);
if (!existingStubs) {
return packet;
}
// Strip escaped characters.
const safeKey = key
.replace(/\\n/g, "\n")
.replace(/\\r/g, "\r")
.replace(/\\\"/g, `\"`)
.replace(/\\\'/g, `\'`);
if (!existingStubs.has(safeKey)) {
return packet;
}
// If the stub already exist, we want to ignore irrelevant properties (generated id, timer, …)
// that might changed and "pollute" the diff resulting from this stub generation.
const existingPacket = existingStubs.get(safeKey);
// copy existing contentDomReference
if (
packet._grip?.contentDomReference?.id &&
existingPacket._grip?.contentDomReference?.id
) {
packet._grip.contentDomReference = existingPacket._grip.contentDomReference;
}
// `window`'s properties count can vary from OS to OS, so we clean `ownPropertyLength`.
if (
existingPacket &&
packet._grip?.class === "Window" &&
typeof packet._grip.ownPropertyLength ==
typeof existingPacket._grip.ownPropertyLength
) {
packet._grip.ownPropertyLength = existingPacket._grip.ownPropertyLength;
}
return packet;
}
// HELPER
const STUBS_FOLDER = "devtools/client/shared/components/test/node/stubs/reps/";
const STUBS_UPDATE_ENV = "STUBS_UPDATE";
/**
* Write stubs to a given file
*
* @param {String} fileName: The file to write the stubs in.
* @param {Map} packets: A Map of the packets.
*/
async function writeStubsToFile(fileName, packets) {
const mozRepo = Services.env.get("MOZ_DEVELOPER_REPO_DIR");
const filePath = `${mozRepo}/${STUBS_FOLDER + fileName}`;
const stubs = Array.from(packets.entries()).map(([key, packet]) => {
const stringifiedPacket = getSerializedPacket(packet);
return `stubs.set(\`${key}\`, ${stringifiedPacket});`;
});
const fileContent = `/* 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
"use strict";
/*
* THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. RUN browser_reps_stubs.js with STUBS_UPDATE=true env TO UPDATE.
*/
const stubs = new Map();
${stubs.join("\n\n")}
module.exports = stubs;
`;
const textEncoder = new TextEncoder();
await IOUtils.write(filePath, textEncoder.encode(fileContent));
}
function getStubFile(fileName) {
return require(CHROME_PREFIX + STUBS_FOLDER + fileName);
}
function sortObjectKeys(obj) {
const isArray = Array.isArray(obj);
const isObject = Object.prototype.toString.call(obj) === "[object Object]";
const isFront = obj?._grip;
if (isObject && !isFront) {
// Reorder keys for objects, but skip fronts to avoid infinite recursion.
const sortedKeys = Object.keys(obj).sort((k1, k2) => k1.localeCompare(k2));
const withSortedKeys = {};
sortedKeys.forEach(k => {
withSortedKeys[k] = k !== "stacktrace" ? sortObjectKeys(obj[k]) : obj[k];
});
return withSortedKeys;
} else if (isArray) {
return obj.map(item => sortObjectKeys(item));
}
return obj;
}
/**
* @param {Object} packet
* The packet to serialize.
* @param {Object} options
* @param {Boolean} options.sortKeys
* Pass true to sort all keys alphabetically in the packet before serialization.
* For instance stub comparison should not fail if the order of properties changed.
* @param {Boolean} options.replaceActorIds
* Pass true to replace actorIDs with a fake one so it's easier to compare stubs
* that includes grips.
*/
function getSerializedPacket(
packet,
{ sortKeys = false, replaceActorIds = false } = {}
) {
if (sortKeys) {
packet = sortObjectKeys(packet);
}
const actorIdPlaceholder = "XXX";
return JSON.stringify(
packet,
function (key, value) {
// The message can have fronts that we need to serialize
if (value && value._grip) {
return {
_grip: value._grip,
actorID: replaceActorIds ? actorIdPlaceholder : value.actorID,
};
}
if (
replaceActorIds &&
(key === "actor" || key === "actorID" || key === "sourceId") &&
typeof value === "string"
) {
return actorIdPlaceholder;
}
return value;
},
2
);
}