summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/test/xpcshell
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/shared/test/xpcshell
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/shared/test/xpcshell')
-rw-r--r--devtools/client/shared/test/xpcshell/.eslintrc.js6
-rw-r--r--devtools/client/shared/test/xpcshell/test_VariablesView_filtering-without-controller.js40
-rw-r--r--devtools/client/shared/test/xpcshell/test_VariablesView_getString_promise.js81
-rw-r--r--devtools/client/shared/test/xpcshell/test_WeakMapMap.js70
-rw-r--r--devtools/client/shared/test/xpcshell/test_advanceValidate.js32
-rw-r--r--devtools/client/shared/test/xpcshell/test_attribute-parsing-01.js76
-rw-r--r--devtools/client/shared/test/xpcshell/test_attribute-parsing-02.js149
-rw-r--r--devtools/client/shared/test/xpcshell/test_bezierCanvas.js123
-rw-r--r--devtools/client/shared/test/xpcshell/test_cssAngle.js32
-rw-r--r--devtools/client/shared/test/xpcshell/test_cssColor-01.js72
-rw-r--r--devtools/client/shared/test/xpcshell/test_cssColor-02.js47
-rw-r--r--devtools/client/shared/test/xpcshell/test_cssColor-03.js61
-rw-r--r--devtools/client/shared/test/xpcshell/test_cssColor-8-digit-hex.js23
-rw-r--r--devtools/client/shared/test/xpcshell/test_cssColorDatabase.js75
-rw-r--r--devtools/client/shared/test/xpcshell/test_cubicBezier.js153
-rw-r--r--devtools/client/shared/test/xpcshell/test_curl.js324
-rw-r--r--devtools/client/shared/test/xpcshell/test_escapeCSSComment.js42
-rw-r--r--devtools/client/shared/test/xpcshell/test_hasCSSVariable.js61
-rw-r--r--devtools/client/shared/test/xpcshell/test_parseDeclarations.js765
-rw-r--r--devtools/client/shared/test/xpcshell/test_parsePseudoClassesAndAttributes.js203
-rw-r--r--devtools/client/shared/test/xpcshell/test_parseSingleValue.js103
-rw-r--r--devtools/client/shared/test/xpcshell/test_rewriteDeclarations.js815
-rw-r--r--devtools/client/shared/test/xpcshell/test_source-utils.js248
-rw-r--r--devtools/client/shared/test/xpcshell/test_suggestion-picker.js147
-rw-r--r--devtools/client/shared/test/xpcshell/test_undoStack.js100
-rw-r--r--devtools/client/shared/test/xpcshell/test_unicode-url.js259
-rw-r--r--devtools/client/shared/test/xpcshell/xpcshell.ini34
27 files changed, 4141 insertions, 0 deletions
diff --git a/devtools/client/shared/test/xpcshell/.eslintrc.js b/devtools/client/shared/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..8611c174f5
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/client/shared/test/xpcshell/test_VariablesView_filtering-without-controller.js b/devtools/client/shared/test/xpcshell/test_VariablesView_filtering-without-controller.js
new file mode 100644
index 0000000000..d819ef847c
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_VariablesView_filtering-without-controller.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that VariablesView._doSearch() works even without an attached
+// VariablesViewController (bug 1196341).
+const { VariablesView } = ChromeUtils.import(
+ "resource://devtools/client/storage/VariablesView.jsm"
+);
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const { globals } = require("devtools/shared/builtin-modules");
+
+const DOMParser = new globals.DOMParser();
+DOMParser.forceEnableXULXBL();
+
+function run_test() {
+ const doc = DOMParser.parseFromString("<div>", "text/html");
+ const container = doc.body.firstChild;
+ ok(container, "Got a container.");
+
+ const vv = new VariablesView(container, { searchEnabled: true });
+ const scope = vv.addScope("Test scope");
+ const item1 = scope.addItem("a", { value: "1" });
+ const item2 = scope.addItem("b", { value: "2" });
+
+ info("Performing a search without a controller.");
+ vv._doSearch("a");
+
+ equal(
+ item1.target.hasAttribute("unmatched"),
+ false,
+ "First item that matched the filter is visible."
+ );
+ equal(
+ item2.target.hasAttribute("unmatched"),
+ true,
+ "The second item that did not match the filter is hidden."
+ );
+}
diff --git a/devtools/client/shared/test/xpcshell/test_VariablesView_getString_promise.js b/devtools/client/shared/test/xpcshell/test_VariablesView_getString_promise.js
new file mode 100644
index 0000000000..6c99c86fe5
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_VariablesView_getString_promise.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { VariablesView } = ChromeUtils.import(
+ "resource://devtools/client/storage/VariablesView.jsm"
+);
+
+const PENDING = {
+ type: "object",
+ class: "Promise",
+ actor: "conn0.obj35",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ promiseState: {
+ state: "pending",
+ },
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownPropertiesLength: 0,
+ safeGetterValues: {},
+ },
+};
+
+const FULFILLED = {
+ type: "object",
+ class: "Promise",
+ actor: "conn0.obj35",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ promiseState: {
+ state: "fulfilled",
+ value: 10,
+ },
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownPropertiesLength: 0,
+ safeGetterValues: {},
+ },
+};
+
+const REJECTED = {
+ type: "object",
+ class: "Promise",
+ actor: "conn0.obj35",
+ extensible: true,
+ frozen: false,
+ sealed: false,
+ promiseState: {
+ state: "rejected",
+ reason: 10,
+ },
+ preview: {
+ kind: "Object",
+ ownProperties: {},
+ ownPropertiesLength: 0,
+ safeGetterValues: {},
+ },
+};
+
+function run_test() {
+ equal(VariablesView.getString(PENDING, { concise: true }), "Promise");
+ equal(VariablesView.getString(PENDING), 'Promise {<state>: "pending"}');
+
+ equal(VariablesView.getString(FULFILLED, { concise: true }), "Promise");
+ equal(
+ VariablesView.getString(FULFILLED),
+ 'Promise {<state>: "fulfilled", <value>: 10}'
+ );
+
+ equal(VariablesView.getString(REJECTED, { concise: true }), "Promise");
+ equal(
+ VariablesView.getString(REJECTED),
+ 'Promise {<state>: "rejected", <reason>: 10}'
+ );
+}
diff --git a/devtools/client/shared/test/xpcshell/test_WeakMapMap.js b/devtools/client/shared/test/xpcshell/test_WeakMapMap.js
new file mode 100644
index 0000000000..f1103ab8f6
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_WeakMapMap.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test WeakMapMap.
+
+"use strict";
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const WeakMapMap = require("devtools/client/shared/WeakMapMap");
+
+const myWeakMapMap = new WeakMapMap();
+const key = { randomObject: true };
+
+// eslint-disable-next-line
+function run_test() {
+ test_set();
+ test_has();
+ test_get();
+ test_delete();
+ test_clear();
+}
+
+function test_set() {
+ // myWeakMapMap.set
+ myWeakMapMap.set(key, "text1", "value1");
+ myWeakMapMap.set(key, "text2", "value2");
+ myWeakMapMap.set(key, "text3", "value3");
+}
+
+function test_has() {
+ // myWeakMapMap.has
+ ok(myWeakMapMap.has(key, "text1"), "text1 exists");
+ ok(myWeakMapMap.has(key, "text2"), "text2 exists");
+ ok(myWeakMapMap.has(key, "text3"), "text3 exists");
+ ok(!myWeakMapMap.has(key, "notakey"), "notakey does not exist");
+}
+
+function test_get() {
+ // myWeakMapMap.get
+ const value1 = myWeakMapMap.get(key, "text1");
+ equal(value1, "value1", "test value1");
+
+ const value2 = myWeakMapMap.get(key, "text2");
+ equal(value2, "value2", "test value2");
+
+ const value3 = myWeakMapMap.get(key, "text3");
+ equal(value3, "value3", "test value3");
+
+ const value4 = myWeakMapMap.get(key, "notakey");
+ equal(value4, undefined, "test value4");
+}
+
+function test_delete() {
+ // myWeakMapMap.delete
+ myWeakMapMap.delete(key, "text2");
+
+ // Check that the correct entry was deleted
+ ok(myWeakMapMap.has(key, "text1"), "text1 exists");
+ ok(!myWeakMapMap.has(key, "text2"), "text2 no longer exists");
+ ok(myWeakMapMap.has(key, "text3"), "text3 exists");
+}
+
+function test_clear() {
+ // myWeakMapMap.clear
+ myWeakMapMap.clear();
+
+ // Ensure myWeakMapMap was properly cleared
+ ok(!myWeakMapMap.has(key, "text1"), "text1 no longer exists");
+ ok(!myWeakMapMap.has(key, "text3"), "text3 no longer exists");
+}
diff --git a/devtools/client/shared/test/xpcshell/test_advanceValidate.js b/devtools/client/shared/test/xpcshell/test_advanceValidate.js
new file mode 100644
index 0000000000..680989d75f
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_advanceValidate.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the advanceValidate function from rule-view.js.
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const { advanceValidate } = require("devtools/client/inspector/shared/utils");
+const { KeyCodes } = require("devtools/client/shared/keycodes");
+
+// 1 2 3
+// 0123456789012345678901234567890
+const sampleInput = '\\symbol "string" url(somewhere)';
+
+function testInsertion(where, result, testName) {
+ info(testName);
+ equal(
+ advanceValidate(KeyCodes.DOM_VK_SEMICOLON, sampleInput, where),
+ result,
+ "testing advanceValidate at " + where
+ );
+}
+
+function run_test() {
+ testInsertion(4, true, "inside a symbol");
+ testInsertion(1, false, "after a backslash");
+ testInsertion(8, true, "after whitespace");
+ testInsertion(11, false, "inside a string");
+ testInsertion(24, false, "inside a URL");
+ testInsertion(31, true, "at the end");
+}
diff --git a/devtools/client/shared/test/xpcshell/test_attribute-parsing-01.js b/devtools/client/shared/test/xpcshell/test_attribute-parsing-01.js
new file mode 100644
index 0000000000..dbe77c6fb0
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_attribute-parsing-01.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test splitBy from node-attribute-parser.js
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const { splitBy } = require("devtools/client/shared/node-attribute-parser");
+
+const TEST_DATA = [
+ {
+ value: "this is a test",
+ splitChar: " ",
+ expected: [
+ { value: "this" },
+ { value: " ", type: "string" },
+ { value: "is" },
+ { value: " ", type: "string" },
+ { value: "a" },
+ { value: " ", type: "string" },
+ { value: "test" },
+ ],
+ },
+ {
+ value: "/path/to/handler",
+ splitChar: " ",
+ expected: [{ value: "/path/to/handler" }],
+ },
+ {
+ value: "test",
+ splitChar: " ",
+ expected: [{ value: "test" }],
+ },
+ {
+ value: " test ",
+ splitChar: " ",
+ expected: [
+ { value: " ", type: "string" },
+ { value: "test" },
+ { value: " ", type: "string" },
+ ],
+ },
+ {
+ value: "",
+ splitChar: " ",
+ expected: [],
+ },
+ {
+ value: " ",
+ splitChar: " ",
+ expected: [
+ { value: " ", type: "string" },
+ { value: " ", type: "string" },
+ { value: " ", type: "string" },
+ ],
+ },
+];
+
+function run_test() {
+ for (const { value, splitChar, expected } of TEST_DATA) {
+ info("Splitting string: " + value);
+ const tokens = splitBy(value, splitChar);
+
+ info("Checking that the number of parsed tokens is correct");
+ Assert.equal(tokens.length, expected.length);
+
+ for (let i = 0; i < tokens.length; i++) {
+ info("Checking the data in token " + i);
+ Assert.equal(tokens[i].value, expected[i].value);
+ if (expected[i].type) {
+ Assert.equal(tokens[i].type, expected[i].type);
+ }
+ }
+ }
+}
diff --git a/devtools/client/shared/test/xpcshell/test_attribute-parsing-02.js b/devtools/client/shared/test/xpcshell/test_attribute-parsing-02.js
new file mode 100644
index 0000000000..96140a2c65
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_attribute-parsing-02.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test parseAttribute from node-attribute-parser.js
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const {
+ parseAttribute,
+} = require("devtools/client/shared/node-attribute-parser");
+
+const TEST_DATA = [
+ {
+ tagName: "body",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "class",
+ attributeValue: "some css class names",
+ expected: [{ value: "some css class names", type: "string" }],
+ },
+ {
+ tagName: "box",
+ namespaceURI:
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ attributeName: "datasources",
+ attributeValue: "/url/1?test=1#test http://mozilla.org/wow",
+ expected: [
+ { value: "/url/1?test=1#test", type: "uri" },
+ { value: " ", type: "string" },
+ { value: "http://mozilla.org/wow", type: "uri" },
+ ],
+ },
+ {
+ tagName: "form",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "action",
+ attributeValue: "/path/to/handler",
+ expected: [{ value: "/path/to/handler", type: "uri" }],
+ },
+ {
+ tagName: "a",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "ping",
+ attributeValue:
+ "http://analytics.com/track?id=54 http://analytics.com/track?id=55",
+ expected: [
+ { value: "http://analytics.com/track?id=54", type: "uri" },
+ { value: " ", type: "string" },
+ { value: "http://analytics.com/track?id=55", type: "uri" },
+ ],
+ },
+ {
+ tagName: "link",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "href",
+ attributeValue: "styles.css",
+ otherAttributes: [{ name: "rel", value: "stylesheet" }],
+ expected: [{ value: "styles.css", type: "cssresource" }],
+ },
+ {
+ tagName: "link",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "href",
+ attributeValue: "styles.css",
+ expected: [{ value: "styles.css", type: "uri" }],
+ },
+ {
+ tagName: "output",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "for",
+ attributeValue: "element-id something id",
+ expected: [
+ { value: "element-id", type: "idref" },
+ { value: " ", type: "string" },
+ { value: "something", type: "idref" },
+ { value: " ", type: "string" },
+ { value: "id", type: "idref" },
+ ],
+ },
+ {
+ tagName: "img",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "contextmenu",
+ attributeValue: "id-of-menu",
+ expected: [{ value: "id-of-menu", type: "idref" }],
+ },
+ {
+ tagName: "img",
+ namespaceURI: "http://www.w3.org/1999/xhtml",
+ attributeName: "src",
+ attributeValue: "omg-thats-so-funny.gif",
+ expected: [{ value: "omg-thats-so-funny.gif", type: "uri" }],
+ },
+ {
+ tagName: "key",
+ namespaceURI:
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ attributeName: "command",
+ attributeValue: "some_command_id",
+ expected: [{ value: "some_command_id", type: "idref" }],
+ },
+ {
+ tagName: "script",
+ namespaceURI: "whatever",
+ attributeName: "src",
+ attributeValue: "script.js",
+ expected: [{ value: "script.js", type: "jsresource" }],
+ },
+];
+
+function run_test() {
+ for (const {
+ tagName,
+ namespaceURI,
+ attributeName,
+ otherAttributes,
+ attributeValue,
+ expected,
+ } of TEST_DATA) {
+ info(
+ "Testing <" + tagName + " " + attributeName + "='" + attributeValue + "'>"
+ );
+
+ const attributes = [
+ ...(otherAttributes || []),
+ { name: attributeName, value: attributeValue },
+ ];
+ const tokens = parseAttribute(
+ namespaceURI,
+ tagName,
+ attributes,
+ attributeName,
+ attributeValue
+ );
+ if (!expected) {
+ Assert.ok(!tokens);
+ continue;
+ }
+
+ info("Checking that the number of parsed tokens is correct");
+ Assert.equal(tokens.length, expected.length);
+
+ for (let i = 0; i < tokens.length; i++) {
+ info("Checking the data in token " + i);
+ Assert.equal(tokens[i].value, expected[i].value);
+ Assert.equal(tokens[i].type, expected[i].type);
+ }
+ }
+}
diff --git a/devtools/client/shared/test/xpcshell/test_bezierCanvas.js b/devtools/client/shared/test/xpcshell/test_bezierCanvas.js
new file mode 100644
index 0000000000..543a345775
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_bezierCanvas.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the BezierCanvas API in the CubicBezierWidget module
+
+var { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+var {
+ CubicBezier,
+ BezierCanvas,
+} = require("devtools/client/shared/widgets/CubicBezierWidget");
+
+function run_test() {
+ offsetsGetterReturnsData();
+ convertsOffsetsToCoordinates();
+ plotsCanvas();
+}
+
+function offsetsGetterReturnsData() {
+ info("offsets getter returns an array of 2 offset objects");
+
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [0.25, 0]);
+ let offsets = b.offsets;
+
+ Assert.equal(offsets.length, 2);
+
+ Assert.ok("top" in offsets[0]);
+ Assert.ok("left" in offsets[0]);
+ Assert.ok("top" in offsets[1]);
+ Assert.ok("left" in offsets[1]);
+
+ Assert.equal(offsets[0].top, "300px");
+ Assert.equal(offsets[0].left, "0px");
+ Assert.equal(offsets[1].top, "100px");
+ Assert.equal(offsets[1].left, "200px");
+
+ info("offsets getter returns data according to current padding");
+
+ b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [0, 0]);
+ offsets = b.offsets;
+
+ Assert.equal(offsets[0].top, "400px");
+ Assert.equal(offsets[0].left, "0px");
+ Assert.equal(offsets[1].top, "0px");
+ Assert.equal(offsets[1].left, "200px");
+}
+
+function convertsOffsetsToCoordinates() {
+ info("Converts offsets to coordinates");
+
+ const b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [0.25, 0]);
+
+ let coordinates = b.offsetsToCoordinates({
+ style: {
+ left: "0px",
+ top: "0px",
+ },
+ });
+ Assert.equal(coordinates.length, 2);
+ Assert.equal(coordinates[0], 0);
+ Assert.equal(coordinates[1], 1.5);
+
+ coordinates = b.offsetsToCoordinates({
+ style: {
+ left: "0px",
+ top: "300px",
+ },
+ });
+ Assert.equal(coordinates[0], 0);
+ Assert.equal(coordinates[1], 0);
+
+ coordinates = b.offsetsToCoordinates({
+ style: {
+ left: "200px",
+ top: "100px",
+ },
+ });
+ Assert.equal(coordinates[0], 1);
+ Assert.equal(coordinates[1], 1);
+}
+
+function plotsCanvas() {
+ info("Plots the curve to the canvas");
+
+ let hasDrawnCurve = false;
+ const b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [0.25, 0]);
+ b.ctx.bezierCurveTo = () => {
+ hasDrawnCurve = true;
+ };
+ b.plot();
+
+ Assert.ok(hasDrawnCurve);
+}
+
+function getCubicBezier() {
+ return new CubicBezier([0, 0, 1, 1]);
+}
+
+function getCanvasMock(w = 200, h = 400) {
+ return {
+ getContext: function() {
+ return {
+ scale: () => {},
+ translate: () => {},
+ clearRect: () => {},
+ beginPath: () => {},
+ closePath: () => {},
+ moveTo: () => {},
+ lineTo: () => {},
+ stroke: () => {},
+ arc: () => {},
+ fill: () => {},
+ bezierCurveTo: () => {},
+ save: () => {},
+ restore: () => {},
+ setTransform: () => {},
+ };
+ },
+ width: w,
+ height: h,
+ };
+}
diff --git a/devtools/client/shared/test/xpcshell/test_cssAngle.js b/devtools/client/shared/test/xpcshell/test_cssAngle.js
new file mode 100644
index 0000000000..bb77ba1b1d
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_cssAngle.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test classifyAngle.
+
+"use strict";
+
+var { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+
+const { angleUtils } = require("devtools/client/shared/css-angle");
+
+const CLASSIFY_TESTS = [
+ { input: "180deg", output: "deg" },
+ { input: "-180deg", output: "deg" },
+ { input: "180DEG", output: "deg" },
+ { input: "200rad", output: "rad" },
+ { input: "-200rad", output: "rad" },
+ { input: "200RAD", output: "rad" },
+ { input: "0.5grad", output: "grad" },
+ { input: "-0.5grad", output: "grad" },
+ { input: "0.5GRAD", output: "grad" },
+ { input: "0.33turn", output: "turn" },
+ { input: "0.33TURN", output: "turn" },
+ { input: "-0.33turn", output: "turn" },
+];
+
+function run_test() {
+ for (const test of CLASSIFY_TESTS) {
+ const result = angleUtils.classifyAngle(test.input);
+ equal(result, test.output, "test classifyAngle(" + test.input + ")");
+ }
+}
diff --git a/devtools/client/shared/test/xpcshell/test_cssColor-01.js b/devtools/client/shared/test/xpcshell/test_cssColor-01.js
new file mode 100644
index 0000000000..71d6506ce5
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_cssColor-01.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test classifyColor.
+
+"use strict";
+
+var { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const { colorUtils } = require("devtools/shared/css/color");
+const InspectorUtils = require("InspectorUtils");
+
+const CLASSIFY_TESTS = [
+ { input: "rgb(255,0,192)", output: "rgb" },
+ { input: "RGB(255,0,192)", output: "rgb" },
+ { input: "RGB(100%,0%,83%)", output: "rgb" },
+ { input: "rgba(255,0,192, 0.25)", output: "rgb" },
+ { input: "hsl(5, 5%, 5%)", output: "hsl" },
+ { input: "hsla(5, 5%, 5%, 0.25)", output: "hsl" },
+ { input: "hSlA(5, 5%, 5%, 0.25)", output: "hsl" },
+ { input: "#f06", output: "hex" },
+ { input: "#f060", output: "hex" },
+ { input: "#fe01cb", output: "hex" },
+ { input: "#fe01cb80", output: "hex" },
+ { input: "#FE01CB", output: "hex" },
+ { input: "#FE01CB80", output: "hex" },
+ { input: "blue", output: "name" },
+ { input: "orange", output: "name" },
+];
+
+function compareWithInspectorUtils(input, isColor) {
+ const ours = colorUtils.colorToRGBA(input);
+ const platform = InspectorUtils.colorToRGBA(input);
+ deepEqual(ours, platform, "color " + input + " matches InspectorUtils");
+ if (isColor) {
+ ok(ours !== null, "'" + input + "' is a color");
+ } else {
+ ok(ours === null, "'" + input + "' is not a color");
+ }
+}
+
+function run_test() {
+ for (const test of CLASSIFY_TESTS) {
+ const result = colorUtils.classifyColor(test.input);
+ equal(result, test.output, "test classifyColor(" + test.input + ")");
+
+ const obj = new colorUtils.CssColor("purple");
+ obj.setAuthoredUnitFromColor(test.input);
+ equal(
+ obj.colorUnit,
+ test.output,
+ "test setAuthoredUnitFromColor(" + test.input + ")"
+ );
+
+ // Check that our implementation matches InspectorUtils.
+ compareWithInspectorUtils(test.input, true);
+
+ // And check some obvious errors.
+ compareWithInspectorUtils("mumble" + test.input, false);
+ compareWithInspectorUtils(test.input + "trailingstuff", false);
+ }
+
+ // Regression test for bug 1303826.
+ const black = new colorUtils.CssColor("#000");
+ black.colorUnit = "name";
+ equal(black.toString(), "black", "test non-upper-case color cycling");
+
+ const upper = new colorUtils.CssColor("BLACK");
+ upper.colorUnit = "hex";
+ equal(upper.toString(), "#000", "test upper-case color cycling");
+ upper.colorUnit = "name";
+ equal(upper.toString(), "BLACK", "test upper-case color preservation");
+}
diff --git a/devtools/client/shared/test/xpcshell/test_cssColor-02.js b/devtools/client/shared/test/xpcshell/test_cssColor-02.js
new file mode 100644
index 0000000000..f50047bc8d
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_cssColor-02.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test color cycling regression - Bug 1303748.
+ *
+ * Values should cycle from a starting value, back to their original values. This can
+ * potentially be a little flaky due to the precision of different color representations.
+ */
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const { colorUtils } = require("devtools/shared/css/color");
+const getFixtureColorData = require("resource://test/helper_color_data.js");
+
+function run_test() {
+ getFixtureColorData().forEach(({ authored, name, hex, hsl, rgb, cycle }) => {
+ if (cycle) {
+ const nameCycled = runCycle(name, cycle);
+ const hexCycled = runCycle(hex, cycle);
+ const hslCycled = runCycle(hsl, cycle);
+ const rgbCycled = runCycle(rgb, cycle);
+ // Cut down on log output by only reporting a single pass/fail for the color.
+ ok(
+ nameCycled && hexCycled && hslCycled && rgbCycled,
+ `${authored} was able to cycle back to the original value`
+ );
+ }
+ });
+}
+
+/**
+ * Test a color cycle to see if a color cycles back to its original value in a fixed
+ * number of steps.
+ *
+ * @param {string} value - The color value, e.g. "#000".
+ * @param {integer) times - The number of times it takes to cycle back to the
+ * original color.
+ */
+function runCycle(value, times) {
+ let color = new colorUtils.CssColor(value);
+ for (let i = 0; i < times; i++) {
+ color.nextColorUnit();
+ color = new colorUtils.CssColor(color.toString());
+ }
+ return color.toString() === value;
+}
diff --git a/devtools/client/shared/test/xpcshell/test_cssColor-03.js b/devtools/client/shared/test/xpcshell/test_cssColor-03.js
new file mode 100644
index 0000000000..f371b95f85
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_cssColor-03.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test css-color-4 color function syntax and old-style syntax.
+
+"use strict";
+
+var { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const { colorUtils } = require("devtools/shared/css/color");
+const InspectorUtils = require("InspectorUtils");
+
+const OLD_STYLE_TESTS = [
+ "rgb(255,0,192)",
+ "RGB(255,0,192)",
+ "RGB(100%,0%,83%)",
+ "rgba(255,0,192,0.25)",
+ "hsl(120, 100%, 40%)",
+ "hsla(120, 100%, 40%, 0.25)",
+ "hSlA(240, 100%, 50%, 0.25)",
+];
+
+const CSS_COLOR_4_TESTS = [
+ "rgb(255.0,0.0,192.0)",
+ "RGB(255 0 192)",
+ "RGB(100% 0% 83% / 0.5)",
+ "RGB(100%,0%,83%,0.5)",
+ "RGB(100%,0%,83%,50%)",
+ "rgba(255,0,192)",
+ "hsl(50deg,15%,25%)",
+ "hsl(240 25% 33%)",
+ "hsl(50deg 25% 33% / 0.25)",
+ "hsl(60 120% 60% / 0.25)",
+ "hSlA(5turn 40% 4%)",
+];
+
+function run_test() {
+ for (const test of OLD_STYLE_TESTS) {
+ const ours = colorUtils.colorToRGBA(test, false);
+ const platform = InspectorUtils.colorToRGBA(test);
+ deepEqual(ours, platform, "color " + test + " matches InspectorUtils");
+ ok(ours !== null, "'" + test + "' is a color");
+ }
+
+ for (const test of CSS_COLOR_4_TESTS) {
+ const oursOld = colorUtils.colorToRGBA(test, false);
+ const oursNew = colorUtils.colorToRGBA(test, true);
+ const platform = InspectorUtils.colorToRGBA(test);
+ notEqual(
+ oursOld,
+ platform,
+ "old style parser for color " + test + " should not match InspectorUtils"
+ );
+ ok(oursOld === null, "'" + test + "' is not a color with old parser");
+ deepEqual(
+ oursNew,
+ platform,
+ `css-color-4 parser for color ${test} matches InspectorUtils`
+ );
+ ok(oursNew !== null, "'" + test + "' is a color with css-color-4 parser");
+ }
+}
diff --git a/devtools/client/shared/test/xpcshell/test_cssColor-8-digit-hex.js b/devtools/client/shared/test/xpcshell/test_cssColor-8-digit-hex.js
new file mode 100644
index 0000000000..7a476f0c2e
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_cssColor-8-digit-hex.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// 8 character hex colors have 256 possible alpha values compared to the
+// standard 100 values possible via rgba() colors. This test ensures that they
+// are stored correctly without any alpha loss.
+
+"use strict";
+
+var { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const { colorUtils } = require("devtools/shared/css/color");
+
+const EIGHT_CHARACTER_HEX = "#fefefef0";
+
+// eslint-disable-next-line
+function run_test() {
+ const cssColor = new colorUtils.CssColor(EIGHT_CHARACTER_HEX);
+ cssColor.colorUnit = colorUtils.CssColor.COLORUNIT.hex;
+
+ const color = cssColor.toString();
+
+ equal(color, EIGHT_CHARACTER_HEX, "alpha value is correct");
+}
diff --git a/devtools/client/shared/test/xpcshell/test_cssColorDatabase.js b/devtools/client/shared/test/xpcshell/test_cssColorDatabase.js
new file mode 100644
index 0000000000..0798086844
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_cssColorDatabase.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that css-color-db matches platform.
+
+"use strict";
+
+var { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+
+const { colorUtils } = require("devtools/shared/css/color");
+const { cssColors } = require("devtools/shared/css/color-db");
+const InspectorUtils = require("InspectorUtils");
+
+function isValid(colorName) {
+ ok(
+ colorUtils.isValidCSSColor(colorName),
+ colorName + " is valid in database"
+ );
+ ok(
+ InspectorUtils.isValidCSSColor(colorName),
+ colorName + " is valid in InspectorUtils"
+ );
+}
+
+function checkOne(colorName, checkName) {
+ const ours = colorUtils.colorToRGBA(colorName);
+ const fromDom = InspectorUtils.colorToRGBA(colorName);
+ deepEqual(ours, fromDom, colorName + " agrees with InspectorUtils");
+
+ isValid(colorName);
+
+ if (checkName) {
+ const { r, g, b } = ours;
+
+ // The color we got might not map back to the same name; but our
+ // implementation should agree with InspectorUtils about which name is
+ // canonical.
+ const ourName = colorUtils.rgbToColorName(r, g, b);
+ const domName = InspectorUtils.rgbToColorName(r, g, b);
+
+ equal(
+ ourName,
+ domName,
+ colorName + " canonical name agrees with InspectorUtils"
+ );
+ }
+}
+
+function run_test() {
+ for (const name in cssColors) {
+ checkOne(name, true);
+ }
+ checkOne("transparent", false);
+
+ // Now check that platform didn't add a new name when we weren't
+ // looking.
+ // XXX Disable this test for now as getCSSValuesForProperty no longer
+ // returns the complete color keyword list.
+ if (false) {
+ const names = InspectorUtils.getCSSValuesForProperty("background-color");
+ for (const name of names) {
+ if (
+ name !== "hsl" &&
+ name !== "hsla" &&
+ name !== "rgb" &&
+ name !== "rgba" &&
+ name !== "inherit" &&
+ name !== "initial" &&
+ name !== "unset"
+ ) {
+ checkOne(name, true);
+ }
+ }
+ }
+}
diff --git a/devtools/client/shared/test/xpcshell/test_cubicBezier.js b/devtools/client/shared/test/xpcshell/test_cubicBezier.js
new file mode 100644
index 0000000000..dd6a718ecc
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_cubicBezier.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the CubicBezier API in the CubicBezierWidget module
+
+var { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+var {
+ CubicBezier,
+ parseTimingFunction,
+} = require("devtools/client/shared/widgets/CubicBezierWidget");
+
+function run_test() {
+ throwsWhenMissingCoordinates();
+ throwsWhenIncorrectCoordinates();
+ convertsStringCoordinates();
+ coordinatesToStringOutputsAString();
+ pointGettersReturnPointCoordinatesArrays();
+ toStringOutputsCubicBezierValue();
+ toStringOutputsCssPresetValues();
+ testParseTimingFunction();
+}
+
+function throwsWhenMissingCoordinates() {
+ do_check_throws(() => {
+ new CubicBezier();
+ }, "Throws an exception when coordinates are missing");
+}
+
+function throwsWhenIncorrectCoordinates() {
+ do_check_throws(() => {
+ new CubicBezier([]);
+ }, "Throws an exception when coordinates are incorrect (empty array)");
+
+ do_check_throws(() => {
+ new CubicBezier([0, 0]);
+ }, "Throws an exception when coordinates are incorrect (incomplete array)");
+
+ do_check_throws(() => {
+ new CubicBezier(["a", "b", "c", "d"]);
+ }, "Throws an exception when coordinates are incorrect (invalid type)");
+
+ do_check_throws(() => {
+ new CubicBezier([1.5, 0, 1.5, 0]);
+ }, "Throws an exception when coordinates are incorrect (time range invalid)");
+
+ do_check_throws(() => {
+ new CubicBezier([-0.5, 0, -0.5, 0]);
+ }, "Throws an exception when coordinates are incorrect (time range invalid)");
+}
+
+function convertsStringCoordinates() {
+ info("Converts string coordinates to numbers");
+ const c = new CubicBezier(["0", "1", ".5", "-2"]);
+
+ Assert.equal(c.coordinates[0], 0);
+ Assert.equal(c.coordinates[1], 1);
+ Assert.equal(c.coordinates[2], 0.5);
+ Assert.equal(c.coordinates[3], -2);
+}
+
+function coordinatesToStringOutputsAString() {
+ info("coordinates.toString() outputs a string representation");
+
+ let c = new CubicBezier(["0", "1", "0.5", "-2"]);
+ let string = c.coordinates.toString();
+ Assert.equal(string, "0,1,.5,-2");
+
+ c = new CubicBezier([1, 1, 1, 1]);
+ string = c.coordinates.toString();
+ Assert.equal(string, "1,1,1,1");
+}
+
+function pointGettersReturnPointCoordinatesArrays() {
+ info("Points getters return arrays of coordinates");
+
+ const c = new CubicBezier([0, 0.2, 0.5, 1]);
+ Assert.equal(c.P1[0], 0);
+ Assert.equal(c.P1[1], 0.2);
+ Assert.equal(c.P2[0], 0.5);
+ Assert.equal(c.P2[1], 1);
+}
+
+function toStringOutputsCubicBezierValue() {
+ info("toString() outputs the cubic-bezier() value");
+
+ const c = new CubicBezier([0, 1, 1, 0]);
+ Assert.equal(c.toString(), "cubic-bezier(0,1,1,0)");
+}
+
+function toStringOutputsCssPresetValues() {
+ info("toString() outputs the css predefined values");
+
+ let c = new CubicBezier([0, 0, 1, 1]);
+ Assert.equal(c.toString(), "linear");
+
+ c = new CubicBezier([0.25, 0.1, 0.25, 1]);
+ Assert.equal(c.toString(), "ease");
+
+ c = new CubicBezier([0.42, 0, 1, 1]);
+ Assert.equal(c.toString(), "ease-in");
+
+ c = new CubicBezier([0, 0, 0.58, 1]);
+ Assert.equal(c.toString(), "ease-out");
+
+ c = new CubicBezier([0.42, 0, 0.58, 1]);
+ Assert.equal(c.toString(), "ease-in-out");
+}
+
+function testParseTimingFunction() {
+ info("test parseTimingFunction");
+
+ for (const test of ["ease", "linear", "ease-in", "ease-out", "ease-in-out"]) {
+ ok(parseTimingFunction(test), test);
+ }
+
+ ok(!parseTimingFunction("something"), "non-function token");
+ ok(!parseTimingFunction("something()"), "non-cubic-bezier function");
+ ok(
+ !parseTimingFunction(
+ "cubic-bezier(something)",
+ "cubic-bezier with non-numeric argument"
+ )
+ );
+ ok(!parseTimingFunction("cubic-bezier(1,2,3:7)", "did not see comma"));
+ ok(!parseTimingFunction("cubic-bezier(1,2,3,7:", "did not see close paren"));
+ ok(!parseTimingFunction("cubic-bezier(1,2", "early EOF after number"));
+ ok(!parseTimingFunction("cubic-bezier(1,2,", "early EOF after comma"));
+ deepEqual(
+ parseTimingFunction("cubic-bezier(1,2,3,7)"),
+ [1, 2, 3, 7],
+ "correct invocation"
+ );
+ deepEqual(
+ parseTimingFunction("cubic-bezier(1, /* */ 2,3, 7 )"),
+ [1, 2, 3, 7],
+ "correct with comments and whitespace"
+ );
+}
+
+function do_check_throws(cb, details) {
+ info(details);
+
+ let hasThrown = false;
+ try {
+ cb();
+ } catch (e) {
+ hasThrown = true;
+ }
+
+ Assert.ok(hasThrown);
+}
diff --git a/devtools/client/shared/test/xpcshell/test_curl.js b/devtools/client/shared/test/xpcshell/test_curl.js
new file mode 100644
index 0000000000..3c54e39db2
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_curl.js
@@ -0,0 +1,324 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests utility functions contained in `source-utils.js`
+ */
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const curl = require("devtools/client/shared/curl");
+const Curl = curl.Curl;
+const CurlUtils = curl.CurlUtils;
+
+const Services = require("Services");
+
+// Test `Curl.generateCommand` headers forwarding/filtering
+add_task(async function() {
+ const request = {
+ url: "https://example.com/form/",
+ method: "GET",
+ headers: [
+ { name: "Host", value: "example.com" },
+ {
+ name: "User-Agent",
+ value:
+ "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0",
+ },
+ { name: "Accept", value: "*/*" },
+ { name: "Accept-Language", value: "en-US,en;q=0.5" },
+ { name: "Accept-Encoding", value: "gzip, deflate, br" },
+ { name: "Origin", value: "https://example.com" },
+ { name: "Connection", value: "keep-alive" },
+ { name: "Referer", value: "https://example.com/home/" },
+ { name: "Content-Type", value: "text/plain" },
+ ],
+ httpVersion: "HTTP/2.0",
+ };
+
+ const cmd = Curl.generateCommand(request);
+ const curlParams = parseCurl(cmd);
+
+ ok(
+ !headerTypeInParams(curlParams, "Host"),
+ "host header ignored - to be generated from url"
+ );
+ ok(
+ exactHeaderInParams(curlParams, "Accept: */*"),
+ "accept header present in curl command"
+ );
+ ok(
+ exactHeaderInParams(
+ curlParams,
+ "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0"
+ ),
+ "user-agent header present in curl command"
+ );
+ ok(
+ exactHeaderInParams(curlParams, "Accept-Language: en-US,en;q=0.5"),
+ "accept-language header present in curl output"
+ );
+ ok(
+ !headerTypeInParams(curlParams, "Accept-Encoding") &&
+ inParams(curlParams, "--compressed"),
+ '"--compressed" param replaced accept-encoding header'
+ );
+ ok(
+ exactHeaderInParams(curlParams, "Origin: https://example.com"),
+ "origin header present in curl output"
+ );
+ ok(
+ exactHeaderInParams(curlParams, "Connection: keep-alive"),
+ "connection header present in curl output"
+ );
+ ok(
+ exactHeaderInParams(curlParams, "Referer: https://example.com/home/"),
+ "referer header present in curl output"
+ );
+ ok(
+ exactHeaderInParams(curlParams, "Content-Type: text/plain"),
+ "content-type header present in curl output"
+ );
+ ok(!inParams(curlParams, "--data"), "no data param in GET curl output");
+ ok(
+ !inParams(curlParams, "--data-raw"),
+ "no raw data param in GET curl output"
+ );
+});
+
+// Test `Curl.generateCommand` URL glob handling
+add_task(async function() {
+ let request = {
+ url: "https://example.com/",
+ method: "GET",
+ headers: [],
+ httpVersion: "HTTP/2.0",
+ };
+
+ let cmd = Curl.generateCommand(request);
+ let curlParams = parseCurl(cmd);
+
+ ok(
+ !inParams(curlParams, "--globoff"),
+ "no globoff param in curl output when not needed"
+ );
+
+ request = {
+ url: "https://example.com/[]",
+ method: "GET",
+ headers: [],
+ httpVersion: "HTTP/2.0",
+ };
+
+ cmd = Curl.generateCommand(request);
+ curlParams = parseCurl(cmd);
+
+ ok(
+ inParams(curlParams, "--globoff"),
+ "globoff param present in curl output when needed"
+ );
+});
+
+// Test `Curl.generateCommand` data POSTing
+add_task(async function() {
+ const request = {
+ url: "https://example.com/form/",
+ method: "POST",
+ headers: [
+ { name: "Content-Length", value: "1000" },
+ { name: "Content-Type", value: "text/plain" },
+ ],
+ httpVersion: "HTTP/2.0",
+ postDataText: "A piece of plain payload text",
+ };
+
+ const cmd = Curl.generateCommand(request);
+ const curlParams = parseCurl(cmd);
+
+ ok(
+ !headerTypeInParams(curlParams, "Content-Length"),
+ "content-length header ignored - curl generates new one"
+ );
+ ok(
+ exactHeaderInParams(curlParams, "Content-Type: text/plain"),
+ "content-type header present in curl output"
+ );
+ ok(
+ inParams(curlParams, "--data-raw"),
+ '"--data-raw" param present in curl output'
+ );
+ ok(
+ inParams(curlParams, `--data-raw ${quote(request.postDataText)}`),
+ "proper payload data present in output"
+ );
+});
+
+// Test `Curl.generateCommand` multipart data POSTing
+add_task(async function() {
+ const boundary = "----------14808";
+ const request = {
+ url: "https://example.com/form/",
+ method: "POST",
+ headers: [
+ {
+ name: "Content-Type",
+ value: `multipart/form-data; boundary=${boundary}`,
+ },
+ ],
+ httpVersion: "HTTP/2.0",
+ postDataText: [
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="field_one"',
+ "",
+ "value_one",
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="field_two"',
+ "",
+ "value two",
+ `--${boundary}--`,
+ "",
+ ].join("\r\n"),
+ };
+
+ const cmd = Curl.generateCommand(request);
+
+ // Check content type
+ const contentTypePos = cmd.indexOf(headerParamPrefix("Content-Type"));
+ const contentTypeParam = headerParam(
+ `Content-Type: multipart/form-data; boundary=${boundary}`
+ );
+ ok(contentTypePos !== -1, "content type header present in curl output");
+ equal(
+ cmd.substr(contentTypePos, contentTypeParam.length),
+ contentTypeParam,
+ "proper content type header present in curl output"
+ );
+
+ // Check binary data
+ const dataBinaryPos = cmd.indexOf("--data-binary");
+ const dataBinaryParam = `--data-binary ${isWin() ? "" : "$"}${escapeNewline(
+ quote(request.postDataText)
+ )}`;
+ ok(dataBinaryPos !== -1, "--data-binary param present in curl output");
+ equal(
+ cmd.substr(dataBinaryPos, dataBinaryParam.length),
+ dataBinaryParam,
+ "proper multipart data present in curl output"
+ );
+});
+
+// Test `CurlUtils.removeBinaryDataFromMultipartText` doesn't change text data
+add_task(async function() {
+ const boundary = "----------14808";
+ const postTextLines = [
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="field_one"',
+ "",
+ "value_one",
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="field_two"',
+ "",
+ "value two",
+ `--${boundary}--`,
+ "",
+ ];
+
+ const cleanedText = CurlUtils.removeBinaryDataFromMultipartText(
+ postTextLines.join("\r\n"),
+ boundary
+ );
+ equal(
+ cleanedText,
+ postTextLines.join("\r\n"),
+ "proper non-binary multipart text unchanged"
+ );
+});
+
+// Test `CurlUtils.removeBinaryDataFromMultipartText` removes binary data
+add_task(async function() {
+ const boundary = "----------14808";
+ const postTextLines = [
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="field_one"',
+ "",
+ "value_one",
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="field_two"; filename="file_field_two.txt"',
+ "",
+ "file content",
+ `--${boundary}--`,
+ "",
+ ];
+
+ const cleanedText = CurlUtils.removeBinaryDataFromMultipartText(
+ postTextLines.join("\r\n"),
+ boundary
+ );
+ postTextLines.splice(7, 1);
+ equal(
+ cleanedText,
+ postTextLines.join("\r\n"),
+ "file content removed from multipart text"
+ );
+});
+
+function isWin() {
+ return Services.appinfo.OS === "WINNT";
+}
+
+const QUOTE = isWin() ? '"' : "'";
+
+// Quote a string, escape the quotes inside the string
+function quote(str) {
+ let escaped;
+ if (isWin()) {
+ escaped = str.replace(new RegExp(QUOTE, "g"), `${QUOTE}${QUOTE}`);
+ } else {
+ escaped = str.replace(new RegExp(QUOTE, "g"), `\\${QUOTE}`);
+ }
+ return QUOTE + escaped + QUOTE;
+}
+
+function escapeNewline(txt) {
+ if (isWin()) {
+ // Add `"` to close quote, then escape newline outside of quote, then start new quote
+ return txt.replace(/[\r\n]{1,2}/g, '"^$&$&"');
+ }
+ return txt.replace(/\r/g, "\\r").replace(/\n/g, "\\n");
+}
+
+// Header param is formatted as -H "Header: value" or -H 'Header: value'
+function headerParam(h) {
+ return "-H " + quote(h);
+}
+
+// Header param prefix is formatted as `-H "HeaderName` or `-H 'HeaderName`
+function headerParamPrefix(headerName) {
+ return `-H ${QUOTE}${headerName}`;
+}
+
+// If any params startswith `-H "HeaderName` or `-H 'HeaderName`
+function headerTypeInParams(curlParams, headerName) {
+ return curlParams.some(param =>
+ param.toLowerCase().startsWith(headerParamPrefix(headerName).toLowerCase())
+ );
+}
+
+function exactHeaderInParams(curlParams, header) {
+ return curlParams.some(param => param === headerParam(header));
+}
+
+function inParams(curlParams, param) {
+ return curlParams.some(p => p.startsWith(param));
+}
+
+// Parse complete curl command to array of params. Can be applied to simple headers/data,
+// but will not on WIN with sophisticated values of --data-binary with e.g. escaped quotes
+function parseCurl(curlCmd) {
+ // This monster regexp parses the command line into an array of arguments,
+ // recognizing quoted args with matching quotes and escaped quotes inside:
+ // [ "curl 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ]
+ const matchRe = /[-A-Za-z1-9]+(?: \$?([\"'])(?:\\\1|.)*?\1)?/g;
+ return curlCmd.match(matchRe);
+}
diff --git a/devtools/client/shared/test/xpcshell/test_escapeCSSComment.js b/devtools/client/shared/test/xpcshell/test_escapeCSSComment.js
new file mode 100644
index 0000000000..91ed4524ea
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_escapeCSSComment.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const {
+ escapeCSSComment,
+ unescapeCSSComment,
+} = require("devtools/shared/css/parsing-utils");
+
+const TEST_DATA = [
+ {
+ input: "simple",
+ expected: "simple",
+ },
+ {
+ input: "/* comment */",
+ expected: "/\\* comment *\\/",
+ },
+ {
+ input: "/* two *//* comments */",
+ expected: "/\\* two *\\//\\* comments *\\/",
+ },
+ {
+ input: "/* nested /\\* comment *\\/ */",
+ expected: "/\\* nested /\\\\* comment *\\\\/ *\\/",
+ },
+];
+
+function run_test() {
+ let i = 0;
+ for (const test of TEST_DATA) {
+ ++i;
+ info("Test #" + i);
+
+ const escaped = escapeCSSComment(test.input);
+ equal(escaped, test.expected);
+ const unescaped = unescapeCSSComment(escaped);
+ equal(unescaped, test.input);
+ }
+}
diff --git a/devtools/client/shared/test/xpcshell/test_hasCSSVariable.js b/devtools/client/shared/test/xpcshell/test_hasCSSVariable.js
new file mode 100644
index 0000000000..21a8d1c3d7
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_hasCSSVariable.js
@@ -0,0 +1,61 @@
+/* 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";
+
+// Test whether hasCSSVariable function of utils.js works correctly or not.
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const {
+ hasCSSVariable,
+} = require("devtools/client/inspector/rules/utils/utils");
+
+function run_test() {
+ info("Normal usage");
+ ok(
+ hasCSSVariable("var(--color)", "--color"),
+ "Found --color variable in var(--color)"
+ );
+ ok(
+ !hasCSSVariable("var(--color)", "--col"),
+ "Did not find --col variable in var(--color)"
+ );
+
+ info("Variable with fallback");
+ ok(
+ hasCSSVariable("var(--color, red)", "--color"),
+ "Found --color variable in var(--color)"
+ );
+ ok(
+ !hasCSSVariable("var(--color, red)", "--col"),
+ "Did not find --col variable in var(--color, red)"
+ );
+
+ info("Nested variables");
+ ok(
+ hasCSSVariable("var(--color1, var(--color2, blue))", "--color1"),
+ "Found --color1 variable in var(--color1, var(--color2, blue))"
+ );
+ ok(
+ hasCSSVariable("var(--color1, var(--color2, blue))", "--color2"),
+ "Found --color2 variable in var(--color1, var(--color2, blue))"
+ );
+ ok(
+ !hasCSSVariable("var(--color1, var(--color2, blue))", "--color"),
+ "Did not find --color variable in var(--color1, var(--color2, blue))"
+ );
+
+ info("Invalid variable");
+ ok(
+ !hasCSSVariable("--color", "--color"),
+ "Did not find --color variable in --color"
+ );
+
+ info("Variable with whitespace");
+ ok(
+ hasCSSVariable("var( --color )", "--color"),
+ "Found --color variable in var( --color )"
+ );
+}
diff --git a/devtools/client/shared/test/xpcshell/test_parseDeclarations.js b/devtools/client/shared/test/xpcshell/test_parseDeclarations.js
new file mode 100644
index 0000000000..d6b9c90b8c
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_parseDeclarations.js
@@ -0,0 +1,765 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const {
+ parseDeclarations,
+ _parseCommentDeclarations,
+ parseNamedDeclarations,
+} = require("devtools/shared/css/parsing-utils");
+const { isCssPropertyKnown } = require("devtools/server/actors/css-properties");
+
+const TEST_DATA = [
+ // Simple test
+ {
+ input: "p:v;",
+ expected: [{ name: "p", value: "v", priority: "", offsets: [0, 4] }],
+ },
+ // Simple test
+ {
+ input: "this:is;a:test;",
+ expected: [
+ { name: "this", value: "is", priority: "", offsets: [0, 8] },
+ { name: "a", value: "test", priority: "", offsets: [8, 15] },
+ ],
+ },
+ // Test a single declaration with semi-colon
+ {
+ input: "name:value;",
+ expected: [
+ { name: "name", value: "value", priority: "", offsets: [0, 11] },
+ ],
+ },
+ // Test a single declaration without semi-colon
+ {
+ input: "name:value",
+ expected: [
+ { name: "name", value: "value", priority: "", offsets: [0, 10] },
+ ],
+ },
+ // Test multiple declarations separated by whitespaces and carriage
+ // returns and tabs
+ {
+ input: "p1 : v1 ; \t\t \n p2:v2; \n\n\n\n\t p3 : v3;",
+ expected: [
+ { name: "p1", value: "v1", priority: "", offsets: [0, 9] },
+ { name: "p2", value: "v2", priority: "", offsets: [16, 22] },
+ { name: "p3", value: "v3", priority: "", offsets: [32, 45] },
+ ],
+ },
+ // Test simple priority
+ {
+ input: "p1: v1; p2: v2 !important;",
+ expected: [
+ { name: "p1", value: "v1", priority: "", offsets: [0, 7] },
+ { name: "p2", value: "v2", priority: "important", offsets: [8, 26] },
+ ],
+ },
+ // Test simple priority
+ {
+ input: "p1: v1 !important; p2: v2",
+ expected: [
+ { name: "p1", value: "v1", priority: "important", offsets: [0, 18] },
+ { name: "p2", value: "v2", priority: "", offsets: [19, 25] },
+ ],
+ },
+ // Test simple priority
+ {
+ input: "p1: v1 ! important; p2: v2 ! important;",
+ expected: [
+ { name: "p1", value: "v1", priority: "important", offsets: [0, 20] },
+ { name: "p2", value: "v2", priority: "important", offsets: [21, 40] },
+ ],
+ },
+ // Test simple priority
+ {
+ input: "p1: v1 !/*comment*/important;",
+ expected: [
+ { name: "p1", value: "v1", priority: "important", offsets: [0, 29] },
+ ],
+ },
+ // Test priority without terminating ";".
+ {
+ input: "p1: v1 !important",
+ expected: [
+ { name: "p1", value: "v1", priority: "important", offsets: [0, 17] },
+ ],
+ },
+ // Test trailing "!" without terminating ";".
+ {
+ input: "p1: v1 !",
+ expected: [{ name: "p1", value: "v1 !", priority: "", offsets: [0, 8] }],
+ },
+ // Test invalid priority
+ {
+ input: "p1: v1 important;",
+ expected: [
+ { name: "p1", value: "v1 important", priority: "", offsets: [0, 17] },
+ ],
+ },
+ // Test invalid priority (in the middle of the declaration).
+ // See bug 1462553.
+ {
+ input: "p1: v1 !important v2;",
+ expected: [
+ { name: "p1", value: "v1 !important v2", priority: "", offsets: [0, 21] },
+ ],
+ },
+ {
+ input: "p1: v1 ! important v2;",
+ expected: [
+ {
+ name: "p1",
+ value: "v1 ! important v2",
+ priority: "",
+ offsets: [0, 25],
+ },
+ ],
+ },
+ {
+ input: "p1: v1 ! /*comment*/ important v2;",
+ expected: [
+ {
+ name: "p1",
+ value: "v1 ! important v2",
+ priority: "",
+ offsets: [0, 36],
+ },
+ ],
+ },
+ {
+ input: "p1: v1 !/*hi*/important v2;",
+ expected: [
+ {
+ name: "p1",
+ value: "v1 ! important v2",
+ priority: "",
+ offsets: [0, 27],
+ },
+ ],
+ },
+ // Test various types of background-image urls
+ {
+ input: "background-image: url(../../relative/image.png)",
+ expected: [
+ {
+ name: "background-image",
+ value: "url(../../relative/image.png)",
+ priority: "",
+ offsets: [0, 47],
+ },
+ ],
+ },
+ {
+ input: "background-image: url(http://site.com/test.png)",
+ expected: [
+ {
+ name: "background-image",
+ value: "url(http://site.com/test.png)",
+ priority: "",
+ offsets: [0, 47],
+ },
+ ],
+ },
+ {
+ input: "background-image: url(wow.gif)",
+ expected: [
+ {
+ name: "background-image",
+ value: "url(wow.gif)",
+ priority: "",
+ offsets: [0, 30],
+ },
+ ],
+ },
+ // Test that urls with :;{} characters in them are parsed correctly
+ {
+ input:
+ 'background: red url("http://site.com/image{}:;.png?id=4#wat") ' +
+ "repeat top right",
+ expected: [
+ {
+ name: "background",
+ value:
+ 'red url("http://site.com/image{}:;.png?id=4#wat") ' +
+ "repeat top right",
+ priority: "",
+ offsets: [0, 78],
+ },
+ ],
+ },
+ // Test that an empty string results in an empty array
+ { input: "", expected: [] },
+ // Test that a string comprised only of whitespaces results in an empty array
+ { input: " \n \n \n \n \t \t\t\t ", expected: [] },
+ // Test that a null input throws an exception
+ { input: null, throws: true },
+ // Test that a undefined input throws an exception
+ { input: undefined, throws: true },
+ // Test that :;{} characters in quoted content are not parsed as multiple
+ // declarations
+ {
+ input: 'content: ";color:red;}selector{color:yellow;"',
+ expected: [
+ {
+ name: "content",
+ value: '";color:red;}selector{color:yellow;"',
+ priority: "",
+ offsets: [0, 45],
+ },
+ ],
+ },
+ // Test that rules aren't parsed, just declarations. So { and } found after a
+ // property name should be part of the property name, same for values.
+ {
+ input: "body {color:red;} p {color: blue;}",
+ expected: [
+ { name: "body {color", value: "red", priority: "", offsets: [0, 16] },
+ { name: "} p {color", value: "blue", priority: "", offsets: [16, 33] },
+ { name: "}", value: "", priority: "", offsets: [33, 34] },
+ ],
+ },
+ // Test unbalanced : and ;
+ {
+ input: "color :red : font : arial;",
+ expected: [
+ {
+ name: "color",
+ value: "red : font : arial",
+ priority: "",
+ offsets: [0, 26],
+ },
+ ],
+ },
+ {
+ input: "background: red;;;;;",
+ expected: [
+ { name: "background", value: "red", priority: "", offsets: [0, 16] },
+ ],
+ },
+ {
+ input: "background:;",
+ expected: [
+ { name: "background", value: "", priority: "", offsets: [0, 12] },
+ ],
+ },
+ { input: ";;;;;", expected: [] },
+ { input: ":;:;", expected: [] },
+ // Test name only
+ {
+ input: "color",
+ expected: [{ name: "color", value: "", priority: "", offsets: [0, 5] }],
+ },
+ // Test trailing name without :
+ {
+ input: "color:blue;font",
+ expected: [
+ { name: "color", value: "blue", priority: "", offsets: [0, 11] },
+ { name: "font", value: "", priority: "", offsets: [11, 15] },
+ ],
+ },
+ // Test trailing name with :
+ {
+ input: "color:blue;font:",
+ expected: [
+ { name: "color", value: "blue", priority: "", offsets: [0, 11] },
+ { name: "font", value: "", priority: "", offsets: [11, 16] },
+ ],
+ },
+ // Test leading value
+ {
+ input: "Arial;color:blue;",
+ expected: [
+ { name: "", value: "Arial", priority: "", offsets: [0, 6] },
+ { name: "color", value: "blue", priority: "", offsets: [6, 17] },
+ ],
+ },
+ // Test hex colors
+ {
+ input: "color: #333",
+ expected: [
+ { name: "color", value: "#333", priority: "", offsets: [0, 11] },
+ ],
+ },
+ {
+ input: "color: #456789",
+ expected: [
+ { name: "color", value: "#456789", priority: "", offsets: [0, 14] },
+ ],
+ },
+ {
+ input: "wat: #XYZ",
+ expected: [{ name: "wat", value: "#XYZ", priority: "", offsets: [0, 9] }],
+ },
+ // Test string/url quotes escaping
+ {
+ input: "content: \"this is a 'string'\"",
+ expected: [
+ {
+ name: "content",
+ value: "\"this is a 'string'\"",
+ priority: "",
+ offsets: [0, 29],
+ },
+ ],
+ },
+ {
+ input: 'content: "this is a \\"string\\""',
+ expected: [
+ {
+ name: "content",
+ value: '"this is a \\"string\\""',
+ priority: "",
+ offsets: [0, 31],
+ },
+ ],
+ },
+ {
+ input: "content: 'this is a \"string\"'",
+ expected: [
+ {
+ name: "content",
+ value: "'this is a \"string\"'",
+ priority: "",
+ offsets: [0, 29],
+ },
+ ],
+ },
+ {
+ input: "content: 'this is a \\'string\\''",
+ expected: [
+ {
+ name: "content",
+ value: "'this is a \\'string\\''",
+ priority: "",
+ offsets: [0, 31],
+ },
+ ],
+ },
+ {
+ input: "content: 'this \\' is a \" really strange string'",
+ expected: [
+ {
+ name: "content",
+ value: "'this \\' is a \" really strange string'",
+ priority: "",
+ offsets: [0, 47],
+ },
+ ],
+ },
+ {
+ input: 'content: "a not s\\ o very long title"',
+ expected: [
+ {
+ name: "content",
+ value: '"a not s\\ o very long title"',
+ priority: "",
+ offsets: [0, 46],
+ },
+ ],
+ },
+ // Test calc with nested parentheses
+ {
+ input: "width: calc((100% - 3em) / 2)",
+ expected: [
+ {
+ name: "width",
+ value: "calc((100% - 3em) / 2)",
+ priority: "",
+ offsets: [0, 29],
+ },
+ ],
+ },
+
+ // Simple embedded comment test.
+ {
+ parseComments: true,
+ input: "width: 5; /* background: green; */ background: red;",
+ expected: [
+ { name: "width", value: "5", priority: "", offsets: [0, 9] },
+ {
+ name: "background",
+ value: "green",
+ priority: "",
+ offsets: [13, 31],
+ commentOffsets: [10, 34],
+ },
+ { name: "background", value: "red", priority: "", offsets: [35, 51] },
+ ],
+ },
+
+ // Embedded comment where the parsing heuristic fails.
+ {
+ parseComments: true,
+ input: "width: 5; /* background something: green; */ background: red;",
+ expected: [
+ { name: "width", value: "5", priority: "", offsets: [0, 9] },
+ { name: "background", value: "red", priority: "", offsets: [45, 61] },
+ ],
+ },
+
+ // Embedded comment where the parsing heuristic is a bit funny.
+ {
+ parseComments: true,
+ input: "width: 5; /* background: */ background: red;",
+ expected: [
+ { name: "width", value: "5", priority: "", offsets: [0, 9] },
+ {
+ name: "background",
+ value: "",
+ priority: "",
+ offsets: [13, 24],
+ commentOffsets: [10, 27],
+ },
+ { name: "background", value: "red", priority: "", offsets: [28, 44] },
+ ],
+ },
+
+ // Another case where the parsing heuristic says not to bother; note
+ // that there is no ";" in the comment.
+ {
+ parseComments: true,
+ input: "width: 5; /* background: yellow */ background: red;",
+ expected: [
+ { name: "width", value: "5", priority: "", offsets: [0, 9] },
+ {
+ name: "background",
+ value: "yellow",
+ priority: "",
+ offsets: [13, 31],
+ commentOffsets: [10, 34],
+ },
+ { name: "background", value: "red", priority: "", offsets: [35, 51] },
+ ],
+ },
+
+ // Parsing a comment should yield text that has been unescaped, and
+ // the offsets should refer to the original text.
+ {
+ parseComments: true,
+ input: "/* content: '*\\/'; */",
+ expected: [
+ {
+ name: "content",
+ value: "'*/'",
+ priority: "",
+ offsets: [3, 18],
+ commentOffsets: [0, 21],
+ },
+ ],
+ },
+
+ // Parsing a comment should yield text that has been unescaped, and
+ // the offsets should refer to the original text. This variant
+ // tests the no-semicolon path.
+ {
+ parseComments: true,
+ input: "/* content: '*\\/' */",
+ expected: [
+ {
+ name: "content",
+ value: "'*/'",
+ priority: "",
+ offsets: [3, 17],
+ commentOffsets: [0, 20],
+ },
+ ],
+ },
+
+ // A comment-in-a-comment should yield the correct offsets.
+ {
+ parseComments: true,
+ input: "/* color: /\\* comment *\\/ red; */",
+ expected: [
+ {
+ name: "color",
+ value: "red",
+ priority: "",
+ offsets: [3, 30],
+ commentOffsets: [0, 33],
+ },
+ ],
+ },
+
+ // HTML comments are not special -- they are just ordinary tokens.
+ {
+ parseComments: true,
+ input: "<!-- color: red; --> color: blue;",
+ expected: [
+ { name: "<!-- color", value: "red", priority: "", offsets: [0, 16] },
+ { name: "--> color", value: "blue", priority: "", offsets: [17, 33] },
+ ],
+ },
+
+ // Don't error on an empty comment.
+ {
+ parseComments: true,
+ input: "/**/",
+ expected: [],
+ },
+
+ // Parsing our special comments skips the name-check heuristic.
+ {
+ parseComments: true,
+ input: "/*! walrus: zebra; */",
+ expected: [
+ {
+ name: "walrus",
+ value: "zebra",
+ priority: "",
+ offsets: [4, 18],
+ commentOffsets: [0, 21],
+ },
+ ],
+ },
+
+ // Regression test for bug 1287620.
+ {
+ input: "color: blue \\9 no\\_need",
+ expected: [
+ {
+ name: "color",
+ value: "blue \\9 no_need",
+ priority: "",
+ offsets: [0, 23],
+ },
+ ],
+ },
+
+ // Regression test for bug 1297890 - don't paste tokens.
+ {
+ parseComments: true,
+ input: "stroke-dasharray: 1/*ThisIsAComment*/2;",
+ expected: [
+ {
+ name: "stroke-dasharray",
+ value: "1 2",
+ priority: "",
+ offsets: [0, 39],
+ },
+ ],
+ },
+
+ // Regression test for bug 1384463 - don't trim significant
+ // whitespace.
+ {
+ // \u00a0 is non-breaking space.
+ input: "\u00a0vertical-align: top",
+ expected: [
+ {
+ name: "\u00a0vertical-align",
+ value: "top",
+ priority: "",
+ offsets: [0, 20],
+ },
+ ],
+ },
+
+ // Regression test for bug 1544223 - take CSS blocks into consideration before handling
+ // ; and : (i.e. don't advance to the property name or value automatically).
+ {
+ input: `--foo: (
+ :doodle {
+ @grid: 30x1 / 18vmin;
+ }
+ :container {
+ perspective: 30vmin;
+ }
+
+ @place-cell: center;
+ @size: 100%;
+
+ :after, :before {
+ content: '';
+ @size: 100%;
+ position: absolute;
+ border: 2.4vmin solid var(--color);
+ border-image: radial-gradient(
+ var(--color), transparent 60%
+ );
+ border-image-width: 4;
+ transform: rotate(@pn(0, 5deg));
+ }
+
+ will-change: transform, opacity;
+ animation: scale-up 15s linear infinite;
+ animation-delay: calc(-15s / @size() * @i());
+ box-shadow: inset 0 0 1em var(--color);
+ border-radius: 50%;
+ filter: var(--filter);
+
+ @keyframes scale-up {
+ 0%, 100% {
+ transform: translateZ(0) scale(0) rotate(0);
+ opacity: 0;
+ }
+ 50% {
+ opacity: 1;
+ }
+ 99% {
+ transform:
+ translateZ(30vmin)
+ scale(1)
+ rotate(-270deg);
+ }
+ }
+ );`,
+ expected: [
+ {
+ name: "--foo",
+ value:
+ "( :doodle { @grid: 30x1 / 18vmin; } :container { perspective: 30vmin; } " +
+ "@place-cell: center; @size: 100%; :after, :before { content: ''; @size: " +
+ "100%; position: absolute; border: 2.4vmin solid var(--color); " +
+ "border-image: radial-gradient( var(--color), transparent 60% ); " +
+ "border-image-width: 4; transform: rotate(@pn(0, 5deg)); } will-change: " +
+ "transform, opacity; animation: scale-up 15s linear infinite; " +
+ "animation-delay: calc(-15s / @size() * @i()); box-shadow: inset 0 0 1em " +
+ "var(--color); border-radius: 50%; filter: var(--filter); @keyframes " +
+ "scale-up { 0%, 100% { transform: translateZ(0) scale(0) rotate(0); " +
+ "opacity: 0; } 50% { opacity: 1; } 99% { transform: translateZ(30vmin) " +
+ "scale(1) rotate(-270deg); } } )",
+ priority: "",
+ offsets: [0, 1036],
+ },
+ ],
+ },
+];
+
+function run_test() {
+ run_basic_tests();
+ run_comment_tests();
+ run_named_tests();
+}
+
+// Test parseDeclarations.
+function run_basic_tests() {
+ for (const test of TEST_DATA) {
+ info("Test input string " + test.input);
+ let output;
+ try {
+ output = parseDeclarations(
+ isCssPropertyKnown,
+ test.input,
+ test.parseComments
+ );
+ } catch (e) {
+ info(
+ "parseDeclarations threw an exception with the given input " + "string"
+ );
+ if (test.throws) {
+ info("Exception expected");
+ Assert.ok(true);
+ } else {
+ info("Exception unexpected\n" + e);
+ Assert.ok(false);
+ }
+ }
+ if (output) {
+ assertOutput(output, test.expected);
+ }
+ }
+}
+
+const COMMENT_DATA = [
+ {
+ input: "content: 'hi",
+ expected: [
+ {
+ name: "content",
+ value: "'hi",
+ priority: "",
+ terminator: "';",
+ offsets: [2, 14],
+ colonOffsets: [9, 11],
+ commentOffsets: [0, 16],
+ },
+ ],
+ },
+ {
+ input: "text that once confounded the parser;",
+ expected: [],
+ },
+];
+
+// Test parseCommentDeclarations.
+function run_comment_tests() {
+ for (const test of COMMENT_DATA) {
+ info("Test input string " + test.input);
+ const output = _parseCommentDeclarations(
+ isCssPropertyKnown,
+ test.input,
+ 0,
+ test.input.length + 4
+ );
+ deepEqual(output, test.expected);
+ }
+}
+
+const NAMED_DATA = [
+ {
+ input: "position:absolute;top50px;height:50px;",
+ expected: [
+ {
+ name: "position",
+ value: "absolute",
+ priority: "",
+ terminator: "",
+ offsets: [0, 18],
+ colonOffsets: [8, 9],
+ },
+ {
+ name: "height",
+ value: "50px",
+ priority: "",
+ terminator: "",
+ offsets: [26, 38],
+ colonOffsets: [32, 33],
+ },
+ ],
+ },
+];
+
+// Test parseNamedDeclarations.
+function run_named_tests() {
+ for (const test of NAMED_DATA) {
+ info("Test input string " + test.input);
+ const output = parseNamedDeclarations(isCssPropertyKnown, test.input, true);
+ info(JSON.stringify(output));
+ deepEqual(output, test.expected);
+ }
+}
+
+function assertOutput(actual, expected) {
+ if (actual.length === expected.length) {
+ for (let i = 0; i < expected.length; i++) {
+ Assert.ok(!!actual[i]);
+ info(
+ "Check that the output item has the expected name, " +
+ "value and priority"
+ );
+ Assert.equal(expected[i].name, actual[i].name);
+ Assert.equal(expected[i].value, actual[i].value);
+ Assert.equal(expected[i].priority, actual[i].priority);
+ deepEqual(expected[i].offsets, actual[i].offsets);
+ if ("commentOffsets" in expected[i]) {
+ deepEqual(expected[i].commentOffsets, actual[i].commentOffsets);
+ }
+ }
+ } else {
+ for (const prop of actual) {
+ info(
+ "Actual output contained: {name: " +
+ prop.name +
+ ", value: " +
+ prop.value +
+ ", priority: " +
+ prop.priority +
+ "}"
+ );
+ }
+ Assert.equal(actual.length, expected.length);
+ }
+}
diff --git a/devtools/client/shared/test/xpcshell/test_parsePseudoClassesAndAttributes.js b/devtools/client/shared/test/xpcshell/test_parsePseudoClassesAndAttributes.js
new file mode 100644
index 0000000000..c659f8596d
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_parsePseudoClassesAndAttributes.js
@@ -0,0 +1,203 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const {
+ parsePseudoClassesAndAttributes,
+ SELECTOR_ATTRIBUTE,
+ SELECTOR_ELEMENT,
+ SELECTOR_PSEUDO_CLASS,
+} = require("devtools/shared/css/parsing-utils");
+
+const TEST_DATA = [
+ // Test that a null input throws an exception
+ {
+ input: null,
+ throws: true,
+ },
+ // Test that a undefined input throws an exception
+ {
+ input: undefined,
+ throws: true,
+ },
+ {
+ input: ":root",
+ expected: [{ value: ":root", type: SELECTOR_PSEUDO_CLASS }],
+ },
+ {
+ input: ".testclass",
+ expected: [{ value: ".testclass", type: SELECTOR_ELEMENT }],
+ },
+ {
+ input: "div p",
+ expected: [{ value: "div p", type: SELECTOR_ELEMENT }],
+ },
+ {
+ input: "div > p",
+ expected: [{ value: "div > p", type: SELECTOR_ELEMENT }],
+ },
+ {
+ input: "a[hidden]",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[hidden]", type: SELECTOR_ATTRIBUTE },
+ ],
+ },
+ {
+ input: "a[hidden=true]",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[hidden=true]", type: SELECTOR_ATTRIBUTE },
+ ],
+ },
+ {
+ input: "a[hidden=true] p:hover",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[hidden=true]", type: SELECTOR_ATTRIBUTE },
+ { value: " p", type: SELECTOR_ELEMENT },
+ { value: ":hover", type: SELECTOR_PSEUDO_CLASS },
+ ],
+ },
+ {
+ input: 'a[checked="true"]',
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: '[checked="true"]', type: SELECTOR_ATTRIBUTE },
+ ],
+ },
+ {
+ input: "a[title~=test]",
+ expected: [
+ { value: "a", type: SELECTOR_ELEMENT },
+ { value: "[title~=test]", type: SELECTOR_ATTRIBUTE },
+ ],
+ },
+ {
+ input: 'h1[hidden="true"][title^="Important"]',
+ expected: [
+ { value: "h1", type: SELECTOR_ELEMENT },
+ { value: '[hidden="true"]', type: SELECTOR_ATTRIBUTE },
+ { value: '[title^="Important"]', type: SELECTOR_ATTRIBUTE },
+ ],
+ },
+ {
+ input: "p:hover",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":hover", type: SELECTOR_PSEUDO_CLASS },
+ ],
+ },
+ {
+ input: "p + .testclass:hover",
+ expected: [
+ { value: "p + .testclass", type: SELECTOR_ELEMENT },
+ { value: ":hover", type: SELECTOR_PSEUDO_CLASS },
+ ],
+ },
+ {
+ input: "p::before",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: "::before", type: SELECTOR_PSEUDO_CLASS },
+ ],
+ },
+ {
+ input: "p:nth-child(2)",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":nth-child(2)", type: SELECTOR_PSEUDO_CLASS },
+ ],
+ },
+ {
+ input: 'p:not([title="test"]) .testclass',
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ':not([title="test"])', type: SELECTOR_PSEUDO_CLASS },
+ { value: " .testclass", type: SELECTOR_ELEMENT },
+ ],
+ },
+ {
+ input: "a\\:hover",
+ expected: [{ value: "a\\:hover", type: SELECTOR_ELEMENT }],
+ },
+ {
+ input: ":not(:lang(it))",
+ expected: [{ value: ":not(:lang(it))", type: SELECTOR_PSEUDO_CLASS }],
+ },
+ {
+ input: "p:not(:lang(it))",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":not(:lang(it))", type: SELECTOR_PSEUDO_CLASS },
+ ],
+ },
+ {
+ input: "p:not(p:lang(it))",
+ expected: [
+ { value: "p", type: SELECTOR_ELEMENT },
+ { value: ":not(p:lang(it))", type: SELECTOR_PSEUDO_CLASS },
+ ],
+ },
+ {
+ input: ":not(:lang(it)",
+ expected: [{ value: ":not(:lang(it)", type: SELECTOR_ELEMENT }],
+ },
+ {
+ input: ":not(:lang(it)))",
+ expected: [
+ { value: ":not(:lang(it))", type: SELECTOR_PSEUDO_CLASS },
+ { value: ")", type: SELECTOR_ELEMENT },
+ ],
+ },
+];
+
+function run_test() {
+ for (const test of TEST_DATA) {
+ dump("Test input string " + test.input + "\n");
+ let output;
+
+ try {
+ output = parsePseudoClassesAndAttributes(test.input);
+ } catch (e) {
+ dump(
+ "parsePseudoClassesAndAttributes threw an exception with the " +
+ "given input string\n"
+ );
+ if (test.throws) {
+ ok(true, "Exception expected");
+ } else {
+ dump();
+ ok(false, "Exception unexpected\n" + e);
+ }
+ }
+
+ if (output) {
+ assertOutput(output, test.expected);
+ }
+ }
+}
+
+function assertOutput(actual, expected) {
+ if (actual.length === expected.length) {
+ for (let i = 0; i < expected.length; i++) {
+ dump("Check that the output item has the expected value and type\n");
+ ok(!!actual[i]);
+ equal(expected[i].value, actual[i].value);
+ equal(expected[i].type, actual[i].type);
+ }
+ } else {
+ for (const prop of actual) {
+ dump(
+ "Actual output contained: {value: " +
+ prop.value +
+ ", type: " +
+ prop.type +
+ "}\n"
+ );
+ }
+ equal(actual.length, expected.length);
+ }
+}
diff --git a/devtools/client/shared/test/xpcshell/test_parseSingleValue.js b/devtools/client/shared/test/xpcshell/test_parseSingleValue.js
new file mode 100644
index 0000000000..bd479b6ffb
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_parseSingleValue.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const { parseSingleValue } = require("devtools/shared/css/parsing-utils");
+const { isCssPropertyKnown } = require("devtools/server/actors/css-properties");
+
+const TEST_DATA = [
+ { input: null, throws: true },
+ { input: undefined, throws: true },
+ { input: "", expected: { value: "", priority: "" } },
+ { input: " \t \t \n\n ", expected: { value: "", priority: "" } },
+ { input: "blue", expected: { value: "blue", priority: "" } },
+ {
+ input: "blue !important",
+ expected: { value: "blue", priority: "important" },
+ },
+ {
+ input: "blue!important",
+ expected: { value: "blue", priority: "important" },
+ },
+ {
+ input: "blue ! important",
+ expected: { value: "blue", priority: "important" },
+ },
+ {
+ input: "blue ! important",
+ expected: { value: "blue", priority: "important" },
+ },
+ { input: "blue !", expected: { value: "blue !", priority: "" } },
+ {
+ input: "blue !mportant",
+ expected: { value: "blue !mportant", priority: "" },
+ },
+ {
+ input: " blue !important ",
+ expected: { value: "blue", priority: "important" },
+ },
+ {
+ input: 'url("http://url.com/whyWouldYouDoThat!important.png") !important',
+ expected: {
+ value: 'url("http://url.com/whyWouldYouDoThat!important.png")',
+ priority: "important",
+ },
+ },
+ {
+ input: 'url("http://url.com/whyWouldYouDoThat!important.png")',
+ expected: {
+ value: 'url("http://url.com/whyWouldYouDoThat!important.png")',
+ priority: "",
+ },
+ },
+ {
+ input: '"content!important" !important',
+ expected: {
+ value: '"content!important"',
+ priority: "important",
+ },
+ },
+ {
+ input: '"content!important"',
+ expected: {
+ value: '"content!important"',
+ priority: "",
+ },
+ },
+ {
+ input: '"all the \\"\'\\\\ special characters"',
+ expected: {
+ value: '"all the \\"\'\\\\ special characters"',
+ priority: "",
+ },
+ },
+];
+
+function run_test() {
+ for (const test of TEST_DATA) {
+ info("Test input value " + test.input);
+ try {
+ const output = parseSingleValue(isCssPropertyKnown, test.input);
+ assertOutput(output, test.expected);
+ } catch (e) {
+ info(
+ "parseSingleValue threw an exception with the given input " + "value"
+ );
+ if (test.throws) {
+ info("Exception expected");
+ Assert.ok(true);
+ } else {
+ info("Exception unexpected\n" + e);
+ Assert.ok(false);
+ }
+ }
+ }
+}
+
+function assertOutput(actual, expected) {
+ info("Check that the output has the expected value and priority");
+ Assert.equal(expected.value, actual.value);
+ Assert.equal(expected.priority, actual.priority);
+}
diff --git a/devtools/client/shared/test/xpcshell/test_rewriteDeclarations.js b/devtools/client/shared/test/xpcshell/test_rewriteDeclarations.js
new file mode 100644
index 0000000000..49861a9c51
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_rewriteDeclarations.js
@@ -0,0 +1,815 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const RuleRewriter = require("devtools/client/fronts/inspector/rule-rewriter");
+const { isCssPropertyKnown } = require("devtools/server/actors/css-properties");
+
+const TEST_DATA = [
+ {
+ desc: "simple set",
+ input: "p:v;",
+ instruction: { type: "set", name: "p", value: "N", priority: "", index: 0 },
+ expected: "p:N;",
+ },
+ {
+ desc: "simple set clearing !important",
+ input: "p:v !important;",
+ instruction: { type: "set", name: "p", value: "N", priority: "", index: 0 },
+ expected: "p:N;",
+ },
+ {
+ desc: "simple set adding !important",
+ input: "p:v;",
+ instruction: {
+ type: "set",
+ name: "p",
+ value: "N",
+ priority: "important",
+ index: 0,
+ },
+ expected: "p:N !important;",
+ },
+ {
+ desc: "simple set between comments",
+ input: "/*color:red;*/ p:v; /*color:green;*/",
+ instruction: { type: "set", name: "p", value: "N", priority: "", index: 1 },
+ expected: "/*color:red;*/ p:N; /*color:green;*/",
+ },
+ // The rule view can generate a "set" with a previously unknown
+ // property index; which should work like "create".
+ {
+ desc: "set at unknown index",
+ input: "a:b; e: f;",
+ instruction: { type: "set", name: "c", value: "d", priority: "", index: 2 },
+ expected: "a:b; e: f;c: d;",
+ },
+ {
+ desc: "simple rename",
+ input: "p:v;",
+ instruction: { type: "rename", name: "p", newName: "q", index: 0 },
+ expected: "q:v;",
+ },
+ // "rename" is passed the name that the user entered, and must do
+ // any escaping necessary to ensure that this is an identifier.
+ {
+ desc: "rename requiring escape",
+ input: "p:v;",
+ instruction: { type: "rename", name: "p", newName: "a b", index: 0 },
+ expected: "a\\ b:v;",
+ },
+ {
+ desc: "simple create",
+ input: "",
+ instruction: {
+ type: "create",
+ name: "p",
+ value: "v",
+ priority: "important",
+ index: 0,
+ enabled: true,
+ },
+ expected: "p: v !important;",
+ },
+ {
+ desc: "create between two properties",
+ input: "a:b; e: f;",
+ instruction: {
+ type: "create",
+ name: "c",
+ value: "d",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "a:b; c: d;e: f;",
+ },
+ // "create" is passed the name that the user entered, and must do
+ // any escaping necessary to ensure that this is an identifier.
+ {
+ desc: "create requiring escape",
+ input: "",
+ instruction: {
+ type: "create",
+ name: "a b",
+ value: "d",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "a\\ b: d;",
+ },
+ {
+ desc: "simple disable",
+ input: "p:v;",
+ instruction: { type: "enable", name: "p", value: false, index: 0 },
+ expected: "/*! p:v; */",
+ },
+ {
+ desc: "simple enable",
+ input: "/* color:v; */",
+ instruction: { type: "enable", name: "color", value: true, index: 0 },
+ expected: "color:v;",
+ },
+ {
+ desc: "enable with following property in comment",
+ input: "/* color:red; color: blue; */",
+ instruction: { type: "enable", name: "color", value: true, index: 0 },
+ expected: "color:red; /* color: blue; */",
+ },
+ {
+ desc: "enable with preceding property in comment",
+ input: "/* color:red; color: blue; */",
+ instruction: { type: "enable", name: "color", value: true, index: 1 },
+ expected: "/* color:red; */ color: blue;",
+ },
+ {
+ desc: "simple remove",
+ input: "a:b;c:d;e:f;",
+ instruction: { type: "remove", name: "c", index: 1 },
+ expected: "a:b;e:f;",
+ },
+ {
+ desc: "disable with comment ender in string",
+ input: "content: '*/';",
+ instruction: { type: "enable", name: "content", value: false, index: 0 },
+ expected: "/*! content: '*\\/'; */",
+ },
+ {
+ desc: "enable with comment ender in string",
+ input: "/* content: '*\\/'; */",
+ instruction: { type: "enable", name: "content", value: true, index: 0 },
+ expected: "content: '*/';",
+ },
+ {
+ desc: "enable requiring semicolon insertion",
+ // Note the lack of a trailing semicolon in the comment.
+ input: "/* color:red */ color: blue;",
+ instruction: { type: "enable", name: "color", value: true, index: 0 },
+ expected: "color:red; color: blue;",
+ },
+ {
+ desc: "create requiring semicolon insertion",
+ // Note the lack of a trailing semicolon.
+ input: "color: red",
+ instruction: {
+ type: "create",
+ name: "a",
+ value: "b",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "color: red;a: b;",
+ },
+
+ // Newline insertion.
+ {
+ desc: "simple newline insertion",
+ input: "\ncolor: red;\n",
+ instruction: {
+ type: "create",
+ name: "a",
+ value: "b",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "\ncolor: red;\na: b;\n",
+ },
+ // Newline insertion.
+ {
+ desc: "semicolon insertion before newline",
+ // Note the lack of a trailing semicolon.
+ input: "\ncolor: red\n",
+ instruction: {
+ type: "create",
+ name: "a",
+ value: "b",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "\ncolor: red;\na: b;\n",
+ },
+ // Newline insertion.
+ {
+ desc: "newline and semicolon insertion",
+ // Note the lack of a trailing semicolon and newline.
+ input: "\ncolor: red",
+ instruction: {
+ type: "create",
+ name: "a",
+ value: "b",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "\ncolor: red;\na: b;\n",
+ },
+
+ // Newline insertion and indentation.
+ {
+ desc: "indentation with create",
+ input: "\n color: red;\n",
+ instruction: {
+ type: "create",
+ name: "a",
+ value: "b",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "\n color: red;\n a: b;\n",
+ },
+ // Newline insertion and indentation.
+ {
+ desc: "indentation plus semicolon insertion before newline",
+ // Note the lack of a trailing semicolon.
+ input: "\n color: red\n",
+ instruction: {
+ type: "create",
+ name: "a",
+ value: "b",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "\n color: red;\n a: b;\n",
+ },
+ {
+ desc: "indentation inserted before trailing whitespace",
+ // Note the trailing whitespace. This could come from a rule
+ // like:
+ // @supports (mumble) {
+ // body {
+ // color: red;
+ // }
+ // }
+ // Here if we create a rule we don't want it to follow
+ // the indentation of the "}".
+ input: "\n color: red;\n ",
+ instruction: {
+ type: "create",
+ name: "a",
+ value: "b",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "\n color: red;\n a: b;\n ",
+ },
+ // Newline insertion and indentation.
+ {
+ desc: "indentation comes from preceding comment",
+ // Note how the comment comes before the declaration.
+ input: "\n /* comment */ color: red\n",
+ instruction: {
+ type: "create",
+ name: "a",
+ value: "b",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "\n /* comment */ color: red;\n a: b;\n",
+ },
+ // Default indentation.
+ {
+ desc: "use of default indentation",
+ input: "\n",
+ instruction: {
+ type: "create",
+ name: "a",
+ value: "b",
+ priority: "",
+ index: 0,
+ enabled: true,
+ },
+ expected: "\n\ta: b;\n",
+ },
+
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion removes newline",
+ input: "a:b;\nc:d;\ne:f;",
+ instruction: { type: "remove", name: "c", index: 1 },
+ expected: "a:b;\ne:f;",
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion remove blank line",
+ input: "\n a:b;\n c:d; \ne:f;",
+ instruction: { type: "remove", name: "c", index: 1 },
+ expected: "\n a:b;\ne:f;",
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion leaves comment",
+ input: "\n a:b;\n /* something */ c:d; \ne:f;",
+ instruction: { type: "remove", name: "c", index: 1 },
+ expected: "\n a:b;\n /* something */ \ne:f;",
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion leaves previous newline",
+ input: "\n a:b;\n c:d; \ne:f;",
+ instruction: { type: "remove", name: "e", index: 2 },
+ expected: "\n a:b;\n c:d; \n",
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion removes trailing whitespace",
+ input: "\n a:b;\n c:d; \n e:f;",
+ instruction: { type: "remove", name: "e", index: 2 },
+ expected: "\n a:b;\n c:d; \n",
+ },
+ // Deletion handles newlines properly.
+ {
+ desc: "deletion preserves indentation",
+ input: " a:b;\n c:d; \n e:f;",
+ instruction: { type: "remove", name: "a", index: 0 },
+ expected: " c:d; \n e:f;",
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable single quote termination",
+ input: "/* content: 'hi */ color: red;",
+ instruction: { type: "enable", name: "content", value: true, index: 0 },
+ expected: "content: 'hi'; color: red;",
+ changed: { 0: "'hi'" },
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create single quote termination",
+ input: "content: 'hi",
+ instruction: {
+ type: "create",
+ name: "color",
+ value: "red",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "content: 'hi';color: red;",
+ changed: { 0: "'hi'" },
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable double quote termination",
+ input: '/* content: "hi */ color: red;',
+ instruction: { type: "enable", name: "content", value: true, index: 0 },
+ expected: 'content: "hi"; color: red;',
+ changed: { 0: '"hi"' },
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create double quote termination",
+ input: 'content: "hi',
+ instruction: {
+ type: "create",
+ name: "color",
+ value: "red",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: 'content: "hi";color: red;',
+ changed: { 0: '"hi"' },
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable url termination",
+ input: "/* background-image: url(something.jpg */ color: red;",
+ instruction: {
+ type: "enable",
+ name: "background-image",
+ value: true,
+ index: 0,
+ },
+ expected: "background-image: url(something.jpg); color: red;",
+ changed: { 0: "url(something.jpg)" },
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create url termination",
+ input: "background-image: url(something.jpg",
+ instruction: {
+ type: "create",
+ name: "color",
+ value: "red",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "background-image: url(something.jpg);color: red;",
+ changed: { 0: "url(something.jpg)" },
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable url single quote termination",
+ input: "/* background-image: url('something.jpg */ color: red;",
+ instruction: {
+ type: "enable",
+ name: "background-image",
+ value: true,
+ index: 0,
+ },
+ expected: "background-image: url('something.jpg'); color: red;",
+ changed: { 0: "url('something.jpg')" },
+ },
+ // Termination insertion corner case.
+ {
+ desc: "create url single quote termination",
+ input: "background-image: url('something.jpg",
+ instruction: {
+ type: "create",
+ name: "color",
+ value: "red",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "background-image: url('something.jpg');color: red;",
+ changed: { 0: "url('something.jpg')" },
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "create url double quote termination",
+ input: '/* background-image: url("something.jpg */ color: red;',
+ instruction: {
+ type: "enable",
+ name: "background-image",
+ value: true,
+ index: 0,
+ },
+ expected: 'background-image: url("something.jpg"); color: red;',
+ changed: { 0: 'url("something.jpg")' },
+ },
+ // Termination insertion corner case.
+ {
+ desc: "enable url double quote termination",
+ input: 'background-image: url("something.jpg',
+ instruction: {
+ type: "create",
+ name: "color",
+ value: "red",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: 'background-image: url("something.jpg");color: red;',
+ changed: { 0: 'url("something.jpg")' },
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "create backslash termination",
+ input: "something: \\",
+ instruction: {
+ type: "create",
+ name: "color",
+ value: "red",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "something: \\\\;color: red;",
+ // The lexer rewrites the token before we see it. However this is
+ // so obscure as to be inconsequential.
+ changed: { 0: "\uFFFD\\" },
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable backslash single quote termination",
+ input: "something: '\\",
+ instruction: {
+ type: "create",
+ name: "color",
+ value: "red",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "something: '\\\\';color: red;",
+ changed: { 0: "'\\\\'" },
+ },
+ {
+ desc: "enable backslash double quote termination",
+ input: 'something: "\\',
+ instruction: {
+ type: "create",
+ name: "color",
+ value: "red",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: 'something: "\\\\";color: red;',
+ changed: { 0: '"\\\\"' },
+ },
+
+ // Termination insertion corner case.
+ {
+ desc: "enable comment termination",
+ input: "something: blah /* comment ",
+ instruction: {
+ type: "create",
+ name: "color",
+ value: "red",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "something: blah /* comment*/; color: red;",
+ },
+
+ // Rewrite a "heuristic override" comment.
+ {
+ desc: "enable with heuristic override comment",
+ input: "/*! walrus: zebra; */",
+ instruction: { type: "enable", name: "walrus", value: true, index: 0 },
+ expected: "walrus: zebra;",
+ },
+
+ // Sanitize a bad value.
+ {
+ desc: "create sanitize unpaired brace",
+ input: "",
+ instruction: {
+ type: "create",
+ name: "p",
+ value: "}",
+ priority: "",
+ index: 0,
+ enabled: true,
+ },
+ expected: "p: \\};",
+ changed: { 0: "\\}" },
+ },
+ // Sanitize a bad value.
+ {
+ desc: "set sanitize unpaired brace",
+ input: "walrus: zebra;",
+ instruction: {
+ type: "set",
+ name: "walrus",
+ value: "{{}}}",
+ priority: "",
+ index: 0,
+ },
+ expected: "walrus: {{}}\\};",
+ changed: { 0: "{{}}\\}" },
+ },
+ // Sanitize a bad value.
+ {
+ desc: "enable sanitize unpaired brace",
+ input: "/*! walrus: }*/",
+ instruction: { type: "enable", name: "walrus", value: true, index: 0 },
+ expected: "walrus: \\};",
+ changed: { 0: "\\}" },
+ },
+
+ // Creating a new declaration does not require an attempt to
+ // terminate a previous commented declaration.
+ {
+ desc: "disabled declaration does not need semicolon insertion",
+ input: "/*! no: semicolon */\n",
+ instruction: {
+ type: "create",
+ name: "walrus",
+ value: "zebra",
+ priority: "",
+ index: 1,
+ enabled: true,
+ },
+ expected: "/*! no: semicolon */\nwalrus: zebra;\n",
+ changed: {},
+ },
+
+ {
+ desc: "create commented-out property",
+ input: "p: v",
+ instruction: {
+ type: "create",
+ name: "shoveler",
+ value: "duck",
+ priority: "",
+ index: 1,
+ enabled: false,
+ },
+ expected: "p: v;/*! shoveler: duck; */",
+ },
+ {
+ desc: "disabled create with comment ender in string",
+ input: "",
+ instruction: {
+ type: "create",
+ name: "content",
+ value: "'*/'",
+ priority: "",
+ index: 0,
+ enabled: false,
+ },
+ expected: "/*! content: '*\\/'; */",
+ },
+
+ {
+ desc: "delete disabled property",
+ input: "\n a:b;\n /* color:#f06; */\n e:f;",
+ instruction: { type: "remove", name: "color", index: 1 },
+ expected: "\n a:b;\n e:f;",
+ },
+ {
+ desc: "delete heuristic-disabled property",
+ input: "\n a:b;\n /*! c:d; */\n e:f;",
+ instruction: { type: "remove", name: "c", index: 1 },
+ expected: "\n a:b;\n e:f;",
+ },
+ {
+ desc: "delete disabled property leaving other disabled property",
+ input: "\n a:b;\n /* color:#f06; background-color: seagreen; */\n e:f;",
+ instruction: { type: "remove", name: "color", index: 1 },
+ expected: "\n a:b;\n /* background-color: seagreen; */\n e:f;",
+ },
+
+ {
+ desc: "regression test for bug 1328016",
+ input: "position:absolute;top50px;height:50px;width:50px;",
+ instruction: {
+ type: "set",
+ name: "width",
+ value: "60px",
+ priority: "",
+ index: 2,
+ },
+ expected: "position:absolute;top50px;height:50px;width:60px;",
+ },
+
+ {
+ desc: "url regression test for bug 1321970",
+ input: "",
+ instruction: {
+ type: "create",
+ name: "p",
+ value: "url(",
+ priority: "",
+ index: 0,
+ enabled: true,
+ },
+ expected: "p: url();",
+ changed: { 0: "url()" },
+ },
+
+ {
+ desc: "url semicolon regression test for bug 1321970",
+ input: "",
+ instruction: {
+ type: "create",
+ name: "p",
+ value: "url(;",
+ priority: "",
+ index: 0,
+ enabled: true,
+ },
+ expected: "p: url();",
+ changed: { 0: "url()" },
+ },
+
+ {
+ desc: "basic regression test for bug 1321970",
+ input: "",
+ instruction: {
+ type: "create",
+ name: "p",
+ value: "(",
+ priority: "",
+ index: 0,
+ enabled: true,
+ },
+ expected: "p: \\(;",
+ changed: { 0: "\\(" },
+ },
+
+ {
+ desc: "unbalanced regression test for bug 1321970",
+ input: "",
+ instruction: {
+ type: "create",
+ name: "p",
+ value: "({[})",
+ priority: "",
+ index: 0,
+ enabled: true,
+ },
+ expected: "p: ({\\[});",
+ changed: { 0: "({\\[})" },
+ },
+
+ {
+ desc: "function regression test for bug 1321970",
+ input: "",
+ instruction: {
+ type: "create",
+ name: "p",
+ value: "func(1,2)",
+ priority: "",
+ index: 0,
+ enabled: true,
+ },
+ expected: "p: func(1,2);",
+ },
+
+ {
+ desc: "function regression test for bug 1355233",
+ input: "",
+ instruction: {
+ type: "create",
+ name: "p",
+ value: "func(",
+ priority: "",
+ index: 0,
+ enabled: true,
+ },
+ expected: "p: func\\(;",
+ changed: { 0: "func\\(" },
+ },
+];
+
+function rewriteDeclarations(inputString, instruction, defaultIndentation) {
+ const rewriter = new RuleRewriter(isCssPropertyKnown, null, inputString);
+ rewriter.defaultIndentation = defaultIndentation;
+
+ switch (instruction.type) {
+ case "rename":
+ rewriter.renameProperty(
+ instruction.index,
+ instruction.name,
+ instruction.newName
+ );
+ break;
+
+ case "enable":
+ rewriter.setPropertyEnabled(
+ instruction.index,
+ instruction.name,
+ instruction.value
+ );
+ break;
+
+ case "create":
+ rewriter.createProperty(
+ instruction.index,
+ instruction.name,
+ instruction.value,
+ instruction.priority,
+ instruction.enabled
+ );
+ break;
+
+ case "set":
+ rewriter.setProperty(
+ instruction.index,
+ instruction.name,
+ instruction.value,
+ instruction.priority
+ );
+ break;
+
+ case "remove":
+ rewriter.removeProperty(instruction.index, instruction.name);
+ break;
+
+ default:
+ throw new Error("unrecognized instruction");
+ }
+
+ return rewriter.getResult();
+}
+
+function run_test() {
+ for (const test of TEST_DATA) {
+ const { changed, text } = rewriteDeclarations(
+ test.input,
+ test.instruction,
+ "\t"
+ );
+ equal(text, test.expected, "output for " + test.desc);
+
+ let expectChanged;
+ if ("changed" in test) {
+ expectChanged = test.changed;
+ } else {
+ expectChanged = {};
+ }
+ deepEqual(changed, expectChanged, "changed result for " + test.desc);
+ }
+}
diff --git a/devtools/client/shared/test/xpcshell/test_source-utils.js b/devtools/client/shared/test/xpcshell/test_source-utils.js
new file mode 100644
index 0000000000..11d7e93ce8
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_source-utils.js
@@ -0,0 +1,248 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests utility functions contained in `source-utils.js`
+ */
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const sourceUtils = require("devtools/client/shared/source-utils");
+
+const CHROME_URLS = [
+ "chrome://foo",
+ "resource://baz",
+ "jar:file:///Users/root",
+];
+
+const CONTENT_URLS = [
+ "http://mozilla.org",
+ "https://mozilla.org",
+ "file:///Users/root",
+ "app://fxosapp",
+ "blob:http://mozilla.org",
+ "blob:https://mozilla.org",
+];
+
+// Test `sourceUtils.parseURL`
+add_task(async function() {
+ let parsed = sourceUtils.parseURL("https://foo.com:8888/boo/bar.js?q=query");
+ equal(parsed.fileName, "bar.js", "parseURL parsed valid fileName");
+ equal(parsed.host, "foo.com:8888", "parseURL parsed valid host");
+ equal(parsed.hostname, "foo.com", "parseURL parsed valid hostname");
+ equal(parsed.port, "8888", "parseURL parsed valid port");
+ equal(
+ parsed.href,
+ "https://foo.com:8888/boo/bar.js?q=query",
+ "parseURL parsed valid href"
+ );
+
+ parsed = sourceUtils.parseURL("https://foo.com");
+ equal(
+ parsed.host,
+ "foo.com",
+ "parseURL parsed valid host when no port given"
+ );
+ equal(
+ parsed.hostname,
+ "foo.com",
+ "parseURL parsed valid hostname when no port given"
+ );
+
+ equal(
+ sourceUtils.parseURL("self-hosted"),
+ null,
+ "parseURL returns `null` for invalid URLs"
+ );
+});
+
+// Test `sourceUtils.isContentScheme`.
+add_task(async function() {
+ for (const url of CHROME_URLS) {
+ ok(
+ !sourceUtils.isContentScheme(url),
+ `${url} correctly identified as not content scheme`
+ );
+ }
+ for (const url of CONTENT_URLS) {
+ ok(
+ sourceUtils.isContentScheme(url),
+ `${url} correctly identified as content scheme`
+ );
+ }
+});
+
+// Test `sourceUtils.isChromeScheme`.
+add_task(async function() {
+ for (const url of CHROME_URLS) {
+ ok(
+ sourceUtils.isChromeScheme(url),
+ `${url} correctly identified as chrome scheme`
+ );
+ }
+ for (const url of CONTENT_URLS) {
+ ok(
+ !sourceUtils.isChromeScheme(url),
+ `${url} correctly identified as not chrome scheme`
+ );
+ }
+});
+
+// Test `sourceUtils.isWASM`.
+add_task(async function() {
+ ok(
+ sourceUtils.isWASM("wasm-function[66240] (?:13870536)"),
+ "wasm function correctly identified"
+ );
+ ok(
+ !sourceUtils.isWASM(CHROME_URLS[0]),
+ `A chrome url does not identify as wasm.`
+ );
+});
+
+// Test `sourceUtils.isDataScheme`.
+add_task(async function() {
+ const dataURI = "data:text/html;charset=utf-8,<!DOCTYPE html></html>";
+ ok(
+ sourceUtils.isDataScheme(dataURI),
+ `${dataURI} correctly identified as data scheme`
+ );
+
+ for (const url of CHROME_URLS) {
+ ok(
+ !sourceUtils.isDataScheme(url),
+ `${url} correctly identified as not data scheme`
+ );
+ }
+ for (const url of CONTENT_URLS) {
+ ok(
+ !sourceUtils.isDataScheme(url),
+ `${url} correctly identified as not data scheme`
+ );
+ }
+});
+
+// Test `sourceUtils.getSourceNames`.
+add_task(async function() {
+ testAbbreviation(
+ "http://example.com/foo/bar/baz/boo.js",
+ "boo.js",
+ "http://example.com/foo/bar/baz/boo.js",
+ "example.com"
+ );
+});
+
+// Test `sourceUtils.getSourceNames`.
+add_task(async function() {
+ // Check length
+ const longMalformedURL = `example.com${new Array(100)
+ .fill("/a")
+ .join("")}/file.js`;
+ ok(
+ sourceUtils.getSourceNames(longMalformedURL).short.length <= 100,
+ "`short` names are capped at 100 characters"
+ );
+
+ testAbbreviation("self-hosted", "self-hosted", "self-hosted");
+ testAbbreviation("", "(unknown)", "(unknown)");
+
+ // Test shortening data URIs, stripping mime/charset
+ testAbbreviation(
+ "data:text/html;charset=utf-8,<!DOCTYPE html></html>",
+ "data:<!DOCTYPE html></html>",
+ "data:text/html;charset=utf-8,<!DOCTYPE html></html>"
+ );
+
+ const longDataURI = `data:image/png;base64,${new Array(100)
+ .fill("a")
+ .join("")}`;
+ const longDataURIShort = sourceUtils.getSourceNames(longDataURI).short;
+
+ // Test shortening data URIs and that the `short` result is capped
+ ok(
+ longDataURIShort.length <= 100,
+ "`short` names are capped at 100 characters for data URIs"
+ );
+ equal(
+ longDataURIShort.substr(0, 10),
+ "data:aaaaa",
+ "truncated data URI short names still have `data:...`"
+ );
+
+ // Test simple URL and cache retrieval by calling the same input multiple times.
+ const testUrl = "http://example.com/foo/bar/baz/boo.js";
+ testAbbreviation(testUrl, "boo.js", testUrl, "example.com");
+ testAbbreviation(testUrl, "boo.js", testUrl, "example.com");
+
+ // Check query and hash and port
+ testAbbreviation(
+ "http://example.com:8888/foo/bar/baz.js?q=query#go",
+ "baz.js",
+ "http://example.com:8888/foo/bar/baz.js",
+ "example.com:8888"
+ );
+
+ // Trailing "/" with nothing beyond host
+ testAbbreviation(
+ "http://example.com/",
+ "/",
+ "http://example.com/",
+ "example.com"
+ );
+
+ // Trailing "/"
+ testAbbreviation(
+ "http://example.com/foo/bar/",
+ "bar",
+ "http://example.com/foo/bar/",
+ "example.com"
+ );
+
+ // Non-extension ending
+ testAbbreviation(
+ "http://example.com/bar",
+ "bar",
+ "http://example.com/bar",
+ "example.com"
+ );
+
+ // Check query
+ testAbbreviation(
+ "http://example.com/foo.js?bar=1&baz=2",
+ "foo.js",
+ "http://example.com/foo.js",
+ "example.com"
+ );
+
+ // Check query with trailing slash
+ testAbbreviation(
+ "http://example.com/foo/?bar=1&baz=2",
+ "foo",
+ "http://example.com/foo/",
+ "example.com"
+ );
+});
+
+// Test for source mapped file name
+add_task(async function() {
+ const { getSourceMappedFile } = sourceUtils;
+ const source = "baz.js";
+ const output = getSourceMappedFile(source);
+ equal(output, "baz.js", "correctly formats file name");
+ // Test for OSX file path
+ const source1 = "/foo/bar/baz.js";
+ const output1 = getSourceMappedFile(source1);
+ equal(output1, "baz.js", "correctly formats Linux file path");
+ // Test for Windows file path
+ const source2 = "Z:\\foo\\bar\\baz.js";
+ const output2 = getSourceMappedFile(source2);
+ equal(output2, "baz.js", "correctly formats Windows file path");
+});
+
+function testAbbreviation(source, short, long, host) {
+ const results = sourceUtils.getSourceNames(source);
+ equal(results.short, short, `${source} has correct "short" name`);
+ equal(results.long, long, `${source} has correct "long" name`);
+ equal(results.host, host, `${source} has correct "host" name`);
+}
diff --git a/devtools/client/shared/test/xpcshell/test_suggestion-picker.js b/devtools/client/shared/test/xpcshell/test_suggestion-picker.js
new file mode 100644
index 0000000000..112fe9ae33
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_suggestion-picker.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test the suggestion-picker helper methods.
+ */
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const {
+ findMostRelevantIndex,
+ findMostRelevantCssPropertyIndex,
+} = require("devtools/client/shared/suggestion-picker");
+
+/**
+ * Run all tests defined below.
+ */
+function run_test() {
+ ensureMostRelevantIndexProvidedByHelperFunction();
+ ensureMostRelevantIndexProvidedByClassMethod();
+ ensureErrorThrownWithInvalidArguments();
+}
+
+/**
+ * Generic test data.
+ */
+const TEST_DATA = [
+ {
+ // Match in sortedItems array.
+ items: ["chrome", "edge", "firefox"],
+ sortedItems: ["firefox", "chrome", "edge"],
+ expectedIndex: 2,
+ },
+ {
+ // No match in sortedItems array.
+ items: ["apple", "oranges", "banana"],
+ sortedItems: ["kiwi", "pear", "peach"],
+ expectedIndex: 0,
+ },
+ {
+ // Empty items array.
+ items: [],
+ sortedItems: ["empty", "arrays", "can't", "have", "relevant", "indexes"],
+ expectedIndex: -1,
+ },
+];
+
+function ensureMostRelevantIndexProvidedByHelperFunction() {
+ info("Running ensureMostRelevantIndexProvidedByHelperFunction()");
+
+ for (const testData of TEST_DATA) {
+ const { items, sortedItems, expectedIndex } = testData;
+ const mostRelevantIndex = findMostRelevantIndex(items, sortedItems);
+ strictEqual(mostRelevantIndex, expectedIndex);
+ }
+}
+
+/**
+ * CSS properties test data.
+ */
+const CSS_TEST_DATA = [
+ {
+ items: [
+ "backface-visibility",
+ "background",
+ "background-attachment",
+ "background-blend-mode",
+ "background-clip",
+ "background-color",
+ "background-image",
+ "background-origin",
+ "background-position",
+ "background-repeat",
+ ],
+ expectedIndex: 1,
+ },
+ {
+ items: [
+ "caption-side",
+ "clear",
+ "clip",
+ "clip-path",
+ "clip-rule",
+ "color",
+ "color-interpolation",
+ "color-interpolation-filters",
+ "content",
+ "counter-increment",
+ ],
+ expectedIndex: 5,
+ },
+ {
+ items: ["direction", "display", "dominant-baseline"],
+ expectedIndex: 1,
+ },
+ {
+ items: [
+ "object-fit",
+ "object-position",
+ "offset-block-end",
+ "offset-block-start",
+ "offset-inline-end",
+ "offset-inline-start",
+ "opacity",
+ "order",
+ "orphans",
+ "outline",
+ ],
+ expectedIndex: 6,
+ },
+ {
+ items: [
+ "white-space",
+ "widows",
+ "width",
+ "will-change",
+ "word-break",
+ "word-spacing",
+ "word-wrap",
+ "writing-mode",
+ ],
+ expectedIndex: 2,
+ },
+];
+
+function ensureMostRelevantIndexProvidedByClassMethod() {
+ info("Running ensureMostRelevantIndexProvidedByClassMethod()");
+
+ for (const testData of CSS_TEST_DATA) {
+ const { items, expectedIndex } = testData;
+ const mostRelevantIndex = findMostRelevantCssPropertyIndex(items);
+ strictEqual(mostRelevantIndex, expectedIndex);
+ }
+}
+
+function ensureErrorThrownWithInvalidArguments() {
+ info("Running ensureErrorThrownWithInvalidTypeArgument()");
+
+ const expectedError = /Please provide valid items and sortedItems arrays\./;
+ // No arguments passed.
+ Assert.throws(() => findMostRelevantIndex(), expectedError);
+ // Invalid arguments passed.
+ Assert.throws(() => findMostRelevantIndex([]), expectedError);
+ Assert.throws(() => findMostRelevantIndex(null, []), expectedError);
+ Assert.throws(() => findMostRelevantIndex([], "string"), expectedError);
+ Assert.throws(() => findMostRelevantIndex("string", []), expectedError);
+}
diff --git a/devtools/client/shared/test/xpcshell/test_undoStack.js b/devtools/client/shared/test/xpcshell/test_undoStack.js
new file mode 100644
index 0000000000..b9ef14fcb8
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_undoStack.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Loader, Require } = ChromeUtils.import(
+ "resource://devtools/shared/base-loader.js"
+);
+
+const loader = new Loader({
+ paths: {
+ devtools: "resource://devtools",
+ },
+ globals: {},
+});
+const require = Require(loader, { id: "undo-test" });
+
+const { UndoStack } = require("devtools/client/shared/undo");
+
+const MAX_SIZE = 5;
+
+function run_test() {
+ let str = "";
+ const stack = new UndoStack(MAX_SIZE);
+
+ function add(ch) {
+ stack.do(
+ function() {
+ str += ch;
+ },
+ function() {
+ str = str.slice(0, -1);
+ }
+ );
+ }
+
+ Assert.ok(!stack.canUndo());
+ Assert.ok(!stack.canRedo());
+
+ // Check adding up to the limit of the size
+ add("a");
+ Assert.ok(stack.canUndo());
+ Assert.ok(!stack.canRedo());
+
+ add("b");
+ add("c");
+ add("d");
+ add("e");
+
+ Assert.equal(str, "abcde");
+
+ // Check a simple undo+redo
+ stack.undo();
+
+ Assert.equal(str, "abcd");
+ Assert.ok(stack.canRedo());
+
+ stack.redo();
+ Assert.equal(str, "abcde");
+ Assert.ok(!stack.canRedo());
+
+ // Check an undo followed by a new action
+ stack.undo();
+ Assert.equal(str, "abcd");
+
+ add("q");
+ Assert.equal(str, "abcdq");
+ Assert.ok(!stack.canRedo());
+
+ stack.undo();
+ Assert.equal(str, "abcd");
+ stack.redo();
+ Assert.equal(str, "abcdq");
+
+ // Revert back to the beginning of the queue...
+ while (stack.canUndo()) {
+ stack.undo();
+ }
+ Assert.equal(str, "");
+
+ // Now put it all back....
+ while (stack.canRedo()) {
+ stack.redo();
+ }
+ Assert.equal(str, "abcdq");
+
+ // Now go over the undo limit...
+ add("1");
+ add("2");
+ add("3");
+
+ Assert.equal(str, "abcdq123");
+
+ // And now undoing the whole stack should only undo 5 actions.
+ while (stack.canUndo()) {
+ stack.undo();
+ }
+
+ Assert.equal(str, "abc");
+}
diff --git a/devtools/client/shared/test/xpcshell/test_unicode-url.js b/devtools/client/shared/test/xpcshell/test_unicode-url.js
new file mode 100644
index 0000000000..256ff2f239
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_unicode-url.js
@@ -0,0 +1,259 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests utility functions contained in `unicode-url.js`
+ */
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+const {
+ getUnicodeUrl,
+ getUnicodeUrlPath,
+ getUnicodeHostname,
+} = require("devtools/client/shared/unicode-url");
+
+// List of URLs used to test Unicode URL conversion
+const TEST_URLS = [
+ // Type: Readable ASCII URLs
+ // Expected: All of Unicode versions should equal to the raw.
+ {
+ raw: "https://example.org",
+ expectedUnicode: "https://example.org",
+ },
+ {
+ raw: "http://example.org",
+ expectedUnicode: "http://example.org",
+ },
+ {
+ raw: "ftp://example.org",
+ expectedUnicode: "ftp://example.org",
+ },
+ {
+ raw: "https://example.org.",
+ expectedUnicode: "https://example.org.",
+ },
+ {
+ raw: "https://example.org/",
+ expectedUnicode: "https://example.org/",
+ },
+ {
+ raw: "https://example.org/test",
+ expectedUnicode: "https://example.org/test",
+ },
+ {
+ raw: "https://example.org/test.html",
+ expectedUnicode: "https://example.org/test.html",
+ },
+ {
+ raw: "https://example.org/test.html?one=1&two=2",
+ expectedUnicode: "https://example.org/test.html?one=1&two=2",
+ },
+ {
+ raw: "https://example.org/test.html#here",
+ expectedUnicode: "https://example.org/test.html#here",
+ },
+ {
+ raw: "https://example.org/test.html?one=1&two=2#here",
+ expectedUnicode: "https://example.org/test.html?one=1&two=2#here",
+ },
+ // Type: Unreadable URLs with either Punycode domain names or URI-encoded
+ // paths
+ // Expected: Unreadable domain names and URI-encoded paths should be converted
+ // to readable Unicode.
+ {
+ raw: "https://xn--g6w.xn--8pv/test.html",
+ // Do not type Unicode characters directly, because this test file isn't
+ // specified with a known encoding.
+ expectedUnicode: "https://\u6e2c.\u672c/test.html",
+ },
+ {
+ raw: "https://example.org/%E6%B8%AC%E8%A9%A6.html",
+ // Do not type Unicode characters directly, because this test file isn't
+ // specified with a known encoding.
+ expectedUnicode: "https://example.org/\u6e2c\u8a66.html",
+ },
+ {
+ raw: "https://example.org/test.html?One=%E4%B8%80",
+ // Do not type Unicode characters directly, because this test file isn't
+ // specified with a known encoding.
+ expectedUnicode: "https://example.org/test.html?One=\u4e00",
+ },
+ {
+ raw: "https://example.org/test.html?%E4%B8%80=1",
+ // Do not type Unicode characters directly, because this test file isn't
+ // specified with a known encoding.
+ expectedUnicode: "https://example.org/test.html?\u4e00=1",
+ },
+ {
+ raw:
+ "https://xn--g6w.xn--8pv/%E6%B8%AC%E8%A9%A6.html" +
+ "?%E4%B8%80=%E4%B8%80" +
+ "#%E6%AD%A4",
+ // Do not type Unicode characters directly, because this test file isn't
+ // specified with a known encoding.
+ expectedUnicode:
+ "https://\u6e2c.\u672c/\u6e2c\u8a66.html" + "?\u4e00=\u4e00" + "#\u6b64",
+ },
+ // Type: data: URIs
+ // Expected: All should not be converted.
+ {
+ raw: "data:text/plain;charset=UTF-8;Hello%20world",
+ expectedUnicode: "data:text/plain;charset=UTF-8;Hello%20world",
+ },
+ {
+ raw: "data:text/plain;charset=UTF-8;%E6%B8%AC%20%E8%A9%A6",
+ expectedUnicode: "data:text/plain;charset=UTF-8;%E6%B8%AC%20%E8%A9%A6",
+ },
+ {
+ raw:
+ "data:image/png;base64,iVBORw0KGgoAAA" +
+ "ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4" +
+ "//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU" +
+ "5ErkJggg==",
+ expectedUnicode:
+ "data:image/png;base64,iVBORw0KGgoAAA" +
+ "ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4" +
+ "//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU" +
+ "5ErkJggg==",
+ },
+ // Type: Malformed URLs
+ // Expected: All should not be converted.
+ {
+ raw: "://example.org/test",
+ expectedUnicode: "://example.org/test",
+ },
+ {
+ raw: "://xn--g6w.xn--8pv/%E6%B8%AC%E8%A9%A6.html" + "?%E4%B8%80=%E4%B8%80",
+ expectedUnicode:
+ "://xn--g6w.xn--8pv/%E6%B8%AC%E8%A9%A6.html" + "?%E4%B8%80=%E4%B8%80",
+ },
+ {
+ // %E8%A9 isn't a valid UTF-8 code, so this URL is malformed.
+ raw: "https://xn--g6w.xn--8pv/%E6%B8%AC%E8%A9",
+ expectedUnicode: "https://xn--g6w.xn--8pv/%E6%B8%AC%E8%A9",
+ },
+];
+
+// List of hostanmes used to test Unicode hostname conversion
+const TEST_HOSTNAMES = [
+ // Type: Readable ASCII hostnames
+ // Expected: All of Unicode versions should equal to the raw.
+ {
+ raw: "example",
+ expectedUnicode: "example",
+ },
+ {
+ raw: "example.org",
+ expectedUnicode: "example.org",
+ },
+ // Type: Unreadable Punycode hostnames
+ // Expected: Punycode should be converted to readable Unicode.
+ {
+ raw: "xn--g6w",
+ // Do not type Unicode characters directly, because this test file isn't
+ // specified with a known encoding.
+ expectedUnicode: "\u6e2c",
+ },
+ {
+ raw: "xn--g6w.xn--8pv",
+ // Do not type Unicode characters directly, because this test file isn't
+ // specified with a known encoding.
+ expectedUnicode: "\u6e2c.\u672c",
+ },
+];
+
+// List of URL paths used to test Unicode URL path conversion
+const TEST_URL_PATHS = [
+ // Type: Readable ASCII URL paths
+ // Expected: All of Unicode versions should equal to the raw.
+ {
+ raw: "test",
+ expectedUnicode: "test",
+ },
+ {
+ raw: "/",
+ expectedUnicode: "/",
+ },
+ {
+ raw: "/test",
+ expectedUnicode: "/test",
+ },
+ {
+ raw: "/test.html?one=1&two=2#here",
+ expectedUnicode: "/test.html?one=1&two=2#here",
+ },
+ // Type: Unreadable URI-encoded URL paths
+ // Expected: URL paths should be converted to readable Unicode.
+ {
+ raw: "/%E6%B8%AC%E8%A9%A6",
+ // Do not type Unicode characters directly, because this test file isn't
+ // specified with a known encoding.
+ expectedUnicode: "/\u6e2c\u8a66",
+ },
+ {
+ raw: "/%E6%B8%AC%E8%A9%A6.html",
+ // Do not type Unicode characters directly, because this test file isn't
+ // specified with a known encoding.
+ expectedUnicode: "/\u6e2c\u8a66.html",
+ },
+ {
+ raw:
+ "/%E6%B8%AC%E8%A9%A6.html" +
+ "?%E4%B8%80=%E4%B8%80&%E4%BA%8C=%E4%BA%8C" +
+ "#%E6%AD%A4",
+ // Do not type Unicode characters directly, because this test file isn't
+ // specified with a known encoding.
+ expectedUnicode:
+ "/\u6e2c\u8a66.html" + "?\u4e00=\u4e00&\u4e8c=\u4e8c" + "#\u6b64",
+ },
+ // Type: Malformed URL paths
+ // Expected: All should not be converted.
+ {
+ // %E8%A9 isn't a valid UTF-8 code, so this URL is malformed.
+ raw: "/%E6%B8%AC%E8%A9",
+ expectedUnicode: "/%E6%B8%AC%E8%A9",
+ },
+];
+
+function run_test() {
+ // Test URLs
+ for (const url of TEST_URLS) {
+ const result = getUnicodeUrl(url.raw);
+ equal(
+ result,
+ url.expectedUnicode,
+ "Test getUnicodeUrl: " +
+ url.raw +
+ " should be unicodized to " +
+ url.expectedUnicode
+ );
+ }
+
+ // Test hostnames
+ for (const hostname of TEST_HOSTNAMES) {
+ const result = getUnicodeHostname(hostname.raw);
+ equal(
+ result,
+ hostname.expectedUnicode,
+ "Test getUnicodeHostname: " +
+ hostname.raw +
+ " should be unicodized to " +
+ hostname.expectedUnicode
+ );
+ }
+
+ // Test URL paths
+ for (const urlPath of TEST_URL_PATHS) {
+ const result = getUnicodeUrlPath(urlPath.raw);
+ equal(
+ result,
+ urlPath.expectedUnicode,
+ "Test getUnicodeUrlPath: " +
+ urlPath.raw +
+ " should be unicodized to " +
+ urlPath.expectedUnicode
+ );
+ }
+}
diff --git a/devtools/client/shared/test/xpcshell/xpcshell.ini b/devtools/client/shared/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..fef719fadb
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/xpcshell.ini
@@ -0,0 +1,34 @@
+[DEFAULT]
+tags = devtools
+head =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+support-files =
+ ../helper_color_data.js
+
+[test_advanceValidate.js]
+[test_attribute-parsing-01.js]
+[test_attribute-parsing-02.js]
+[test_bezierCanvas.js]
+[test_cssAngle.js]
+[test_cssColor-01.js]
+[test_cssColor-02.js]
+[test_cssColor-03.js]
+[test_cssColor-8-digit-hex.js]
+[test_cssColorDatabase.js]
+[test_cubicBezier.js]
+[test_curl.js]
+[test_escapeCSSComment.js]
+[test_parseDeclarations.js]
+[test_parsePseudoClassesAndAttributes.js]
+[test_parseSingleValue.js]
+[test_hasCSSVariable.js]
+[test_rewriteDeclarations.js]
+[test_source-utils.js]
+[test_suggestion-picker.js]
+[test_undoStack.js]
+[test_unicode-url.js]
+[test_VariablesView_filtering-without-controller.js]
+[test_VariablesView_getString_promise.js]
+[test_WeakMapMap.js]