summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/components/test/browser/browser_reps_stubs.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/components/test/browser/browser_reps_stubs.js')
-rw-r--r--devtools/client/shared/components/test/browser/browser_reps_stubs.js406
1 files changed, 406 insertions, 0 deletions
diff --git a/devtools/client/shared/components/test/browser/browser_reps_stubs.js b/devtools/client/shared/components/test/browser/browser_reps_stubs.js
new file mode 100644
index 0000000000..29b56ab151
--- /dev/null
+++ b/devtools/client/shared/components/test/browser/browser_reps_stubs.js
@@ -0,0 +1,406 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ 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";
+ link.href = "https://example.com/styles.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})
+ );
+ iframe.src = "http://example.org/document-builder.sjs?html=example.org";
+ document.body.append(iframe);
+ await onLoaded;
+ return iframe.contentWindow;
+ })()
+ `,
+ async: true,
+ },
+ ],
+ [
+ "CrossOriginIframeTopWindow",
+ {
+ expression: `window.top`,
+ iframeUrlForExecution:
+ "https://example.net/document-builder.sjs?html=example.net",
+ },
+ ],
+ ]),
+ // XXX: File a bug blocking Bug 1671400 for enabling automatic generation for one of
+ // 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 CHROME_PREFIX = "chrome://mochitests/content/browser/";
+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
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+"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
+ );
+}