diff --git a/.eslintignore b/.eslintignore
index 4d710e107b55..0cc0e1b0f0a7 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -74,6 +74,7 @@ devtools/client/preferences/
devtools/shared/css/generated/properties-db.js
devtools/client/webconsole/test/node/fixtures/stubs/*.js
!devtools/client/webconsole/test/node/fixtures/stubs/index.js
+devtools/client/shared/components/test/node/stubs/reps/*.js
# Ignore devtools files testing sourcemaps / code style
devtools/client/framework/test/code_*
diff --git a/devtools/client/shared/components/moz.build b/devtools/client/shared/components/moz.build
index 2dc4c579f30b..a515d57c0d20 100644
--- a/devtools/client/shared/components/moz.build
+++ b/devtools/client/shared/components/moz.build
@@ -34,4 +34,7 @@ DevToolsModules(
)
MOCHITEST_CHROME_MANIFESTS += ["test/chrome/chrome.ini"]
-BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"]
+BROWSER_CHROME_MANIFESTS += [
+ "test/browser/browser.ini",
+ "test/node/stubs/reps/stubs.ini",
+]
diff --git a/devtools/client/shared/components/test/browser/browser.ini b/devtools/client/shared/components/test/browser/browser.ini
index a4797d206080..619662db8f88 100644
--- a/devtools/client/shared/components/test/browser/browser.ini
+++ b/devtools/client/shared/components/test/browser/browser.ini
@@ -6,3 +6,4 @@ support-files =
!/devtools/client/shared/test/telemetry-test-helpers.js
[browser_notification_box_basic.js]
+[browser_reps_stubs.js]
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 000000000000..e0bc303fd52e
--- /dev/null
+++ b/devtools/client/shared/components/test/browser/browser_reps_stubs.js
@@ -0,0 +1,218 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../../../shared/test/shared-head.js */
+
+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 = {
+ "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`],
+ ]),
+ "undefined.js": new Map([["Undefined", `undefined`]]),
+ // XXX: File a bug blocking Bug 1671400 for enabling automatic generation for one of
+ // the following file.
+ // "accessible.js",
+ // "accessor.js",
+ // "attribute.js",
+ // "big-int.js",
+ // "comment-node.js",
+ // "date-time.js",
+ // "document-type.js",
+ // "document.js",
+ // "element-node.js",
+ // "error.js",
+ // "event.js",
+ // "failure.js",
+ // "function.js",
+ // "grip-array.js",
+ // "grip-map-entry.js",
+ // "grip-map.js",
+ // "grip.js",
+ // "long-string.js",
+ // "object-with-text.js",
+ // "object-with-url.js",
+ // "promise.js",
+ // "regexp.js",
+ // "stylesheet.js",
+ // "symbol.js",
+ // "text-node.js",
+ // "window.js",
+};
+
+add_task(async function() {
+ const isStubsUpdate = 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(env, 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 });
+ const grip = getSerializedPacket(existingStubs.get(key), {
+ sortKeys: 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, expression] of EXPRESSIONS_BY_FILE[stubFile]) {
+ const { result } = await commands.scriptCommand.execute(expression);
+ stubs.set(key, getCleanedPacket(key, result));
+ }
+
+ return stubs;
+}
+
+function getCleanedPacket(key, packet) {
+ // TODO: Remove / normalize any data that is not stable
+ 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 {Object} env
+ * @param {String} fileName: The file to write the stubs in.
+ * @param {Map} packets: A Map of the packets.
+ */
+async function writeStubsToFile(env, fileName, packets) {
+ const mozRepo = 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 . */
+
+"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}
+ * - {Boolean} 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.
+ */
+function getSerializedPacket(packet, { sortKeys = false } = {}) {
+ if (sortKeys) {
+ packet = sortObjectKeys(packet);
+ }
+
+ return JSON.stringify(
+ packet,
+ function(_, value) {
+ // The message can have fronts that we need to serialize
+ if (value && value._grip) {
+ return { _grip: value._grip, actorID: value.actorID };
+ }
+
+ return value;
+ },
+ 2
+ );
+}
diff --git a/devtools/client/shared/components/test/node/components/reps/number.test.js b/devtools/client/shared/components/test/node/components/reps/number.test.js
index 86754a208942..1d87498f1512 100644
--- a/devtools/client/shared/components/test/node/components/reps/number.test.js
+++ b/devtools/client/shared/components/test/node/components/reps/number.test.js
@@ -67,7 +67,7 @@ describe("Boolean", () => {
describe("Negative Zero", () => {
const stubNegativeZeroGrip = stubs.get("NegZeroGrip");
- const stubNegativeZeroValue = stubs.get("NegZeroValue");
+ const stubNegativeZeroValue = -0;
it("correctly selects Number Rep for negative zero grip", () => {
expect(getRep(stubNegativeZeroGrip)).toBe(Number.rep);
@@ -121,12 +121,11 @@ describe("Zero", () => {
});
describe("Unsafe Int", () => {
- const stub = stubs.get("UnsafeInt");
-
it("renders with expected test content for a long number", () => {
const renderedComponent = shallow(
Rep({
- object: stub,
+ // eslint-disable-next-line no-loss-of-precision
+ object: 900719925474099122,
shouldRenderTooltip: true,
})
);
diff --git a/devtools/client/shared/components/test/node/stubs/reps/browser_dummy.js b/devtools/client/shared/components/test/node/stubs/reps/browser_dummy.js
new file mode 100644
index 000000000000..8a9353cd7e3a
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/browser_dummy.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file is a fake test so we can have support files in the stubs.ini, which are then
+// referenced as support files in the webconsole mochitest ini file.
+
+"use strict";
+
+add_task(function() {
+ ok(true, "this is not a test");
+});
diff --git a/devtools/client/shared/components/test/node/stubs/reps/infinity.js b/devtools/client/shared/components/test/node/stubs/reps/infinity.js
index b7483f4ea85e..06deb944dc3b 100644
--- a/devtools/client/shared/components/test/node/stubs/reps/infinity.js
+++ b/devtools/client/shared/components/test/node/stubs/reps/infinity.js
@@ -3,12 +3,16 @@
* file, You can obtain one at . */
"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.set("Infinity", {
+stubs.set(`Infinity`, {
type: "Infinity",
});
-stubs.set("NegativeInfinity", {
+
+stubs.set(`NegativeInfinity`, {
type: "-Infinity",
});
diff --git a/devtools/client/shared/components/test/node/stubs/reps/nan.js b/devtools/client/shared/components/test/node/stubs/reps/nan.js
index 67d188022e23..a0b8b7ab1d26 100644
--- a/devtools/client/shared/components/test/node/stubs/reps/nan.js
+++ b/devtools/client/shared/components/test/node/stubs/reps/nan.js
@@ -3,9 +3,12 @@
* file, You can obtain one at . */
"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.set("NaN", {
+stubs.set(`NaN`, {
type: "NaN",
});
diff --git a/devtools/client/shared/components/test/node/stubs/reps/null.js b/devtools/client/shared/components/test/node/stubs/reps/null.js
index 20b9985e000a..1af8d8ee1807 100644
--- a/devtools/client/shared/components/test/node/stubs/reps/null.js
+++ b/devtools/client/shared/components/test/node/stubs/reps/null.js
@@ -3,9 +3,12 @@
* file, You can obtain one at . */
"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.set("Null", {
+stubs.set(`Null`, {
type: "null",
});
diff --git a/devtools/client/shared/components/test/node/stubs/reps/number.js b/devtools/client/shared/components/test/node/stubs/reps/number.js
index f942a7e89715..05c7c6dd7ef6 100644
--- a/devtools/client/shared/components/test/node/stubs/reps/number.js
+++ b/devtools/client/shared/components/test/node/stubs/reps/number.js
@@ -3,16 +3,19 @@
* file, You can obtain one at . */
"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.set("Int", 5);
-stubs.set("True", true);
-stubs.set("False", false);
-stubs.set("NegZeroValue", -0);
-stubs.set("NegZeroGrip", {
+stubs.set(`Int`, 5);
+
+stubs.set(`True`, true);
+
+stubs.set(`False`, false);
+
+stubs.set(`NegZeroGrip`, {
type: "-0",
});
-// eslint-disable-next-line no-loss-of-precision
-stubs.set("UnsafeInt", 900719925474099122);
module.exports = stubs;
diff --git a/devtools/client/shared/components/test/node/stubs/reps/stubs.ini b/devtools/client/shared/components/test/node/stubs/reps/stubs.ini
new file mode 100644
index 000000000000..e0909160efb4
--- /dev/null
+++ b/devtools/client/shared/components/test/node/stubs/reps/stubs.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ infinity.js
+ nan.js
+ null.js
+ number.js
+ undefined.js
+
+[browser_dummy.js]
+skip-if=true #This is only here so we can expose the support files in other ini files.
diff --git a/devtools/client/shared/components/test/node/stubs/reps/undefined.js b/devtools/client/shared/components/test/node/stubs/reps/undefined.js
index f07d62db712e..922dccafa546 100644
--- a/devtools/client/shared/components/test/node/stubs/reps/undefined.js
+++ b/devtools/client/shared/components/test/node/stubs/reps/undefined.js
@@ -3,9 +3,12 @@
* file, You can obtain one at . */
"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.set("Undefined", {
+stubs.set(`Undefined`, {
type: "undefined",
});