summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/test/unit
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/test/unit')
-rw-r--r--devtools/client/shared/test/unit/.eslintrc.js6
-rw-r--r--devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js36
-rw-r--r--devtools/client/shared/test/unit/test_VariablesView_getString_promise.js76
-rw-r--r--devtools/client/shared/test/unit/test_advanceValidate.js31
-rw-r--r--devtools/client/shared/test/unit/test_attribute-parsing-01.js73
-rw-r--r--devtools/client/shared/test/unit/test_attribute-parsing-02.js134
-rw-r--r--devtools/client/shared/test/unit/test_bezierCanvas.js117
-rw-r--r--devtools/client/shared/test/unit/test_cssAngle.js33
-rw-r--r--devtools/client/shared/test/unit/test_cssColor-01.js76
-rw-r--r--devtools/client/shared/test/unit/test_cssColor-02.js45
-rw-r--r--devtools/client/shared/test/unit/test_cssColor-03.js61
-rw-r--r--devtools/client/shared/test/unit/test_cssColorDatabase.js63
-rw-r--r--devtools/client/shared/test/unit/test_cubicBezier.js146
-rw-r--r--devtools/client/shared/test/unit/test_escapeCSSComment.js40
-rw-r--r--devtools/client/shared/test/unit/test_parseDeclarations.js439
-rw-r--r--devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js213
-rw-r--r--devtools/client/shared/test/unit/test_parseSingleValue.js93
-rw-r--r--devtools/client/shared/test/unit/test_rewriteDeclarations.js529
-rw-r--r--devtools/client/shared/test/unit/test_source-utils.js181
-rw-r--r--devtools/client/shared/test/unit/test_suggestion-picker.js149
-rw-r--r--devtools/client/shared/test/unit/test_undoStack.js98
-rw-r--r--devtools/client/shared/test/unit/xpcshell.ini30
22 files changed, 2669 insertions, 0 deletions
diff --git a/devtools/client/shared/test/unit/.eslintrc.js b/devtools/client/shared/test/unit/.eslintrc.js
new file mode 100644
index 000000000..59adf410a
--- /dev/null
+++ b/devtools/client/shared/test/unit/.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/unit/test_VariablesView_filtering-without-controller.js b/devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js
new file mode 100644
index 000000000..5f4438234
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_VariablesView_filtering-without-controller.js
@@ -0,0 +1,36 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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).
+
+var Cu = Components.utils;
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+const DOMParser = Cc["@mozilla.org/xmlextras/domparser;1"]
+ .createInstance(Ci.nsIDOMParser);
+const { VariablesView } =
+ Cu.import("resource://devtools/client/shared/widgets/VariablesView.jsm", {});
+
+function run_test() {
+ let doc = DOMParser.parseFromString("<div>", "text/html");
+ let container = doc.body.firstChild;
+ ok(container, "Got a container.");
+
+ let vv = new VariablesView(container, { searchEnabled: true });
+ let scope = vv.addScope("Test scope");
+ let item1 = scope.addItem("a", { value: "1" });
+ let item2 = scope.addItem("b", { value: "2" });
+
+ do_print("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/unit/test_VariablesView_getString_promise.js b/devtools/client/shared/test/unit/test_VariablesView_getString_promise.js
new file mode 100644
index 000000000..a70c870bb
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_VariablesView_getString_promise.js
@@ -0,0 +1,76 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { VariablesView } = Components.utils.import("resource://devtools/client/shared/widgets/VariablesView.jsm", {});
+
+const PENDING = {
+ "type": "object",
+ "class": "Promise",
+ "actor": "conn0.pausedobj35",
+ "extensible": true,
+ "frozen": false,
+ "sealed": false,
+ "promiseState": {
+ "state": "pending"
+ },
+ "preview": {
+ "kind": "Object",
+ "ownProperties": {},
+ "ownPropertiesLength": 0,
+ "safeGetterValues": {}
+ }
+};
+
+const FULFILLED = {
+ "type": "object",
+ "class": "Promise",
+ "actor": "conn0.pausedobj35",
+ "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.pausedobj35",
+ "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/unit/test_advanceValidate.js b/devtools/client/shared/test/unit/test_advanceValidate.js
new file mode 100644
index 000000000..2b3122a6f
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_advanceValidate.js
@@ -0,0 +1,31 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 {utils: Cu, interfaces: Ci} = Components;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {advanceValidate} = require("devtools/client/inspector/shared/utils");
+
+// 1 2 3
+// 0123456789012345678901234567890
+const sampleInput = '\\symbol "string" url(somewhere)';
+
+function testInsertion(where, result, testName) {
+ do_print(testName);
+ equal(advanceValidate(Ci.nsIDOMKeyEvent.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/unit/test_attribute-parsing-01.js b/devtools/client/shared/test/unit/test_attribute-parsing-01.js
new file mode 100644
index 000000000..b6b1a301d
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_attribute-parsing-01.js
@@ -0,0 +1,73 @@
+/* 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} = Components.utils.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 (let {value, splitChar, expected} of TEST_DATA) {
+ do_print("Splitting string: " + value);
+ let tokens = splitBy(value, splitChar);
+
+ do_print("Checking that the number of parsed tokens is correct");
+ do_check_eq(tokens.length, expected.length);
+
+ for (let i = 0; i < tokens.length; i++) {
+ do_print("Checking the data in token " + i);
+ do_check_eq(tokens[i].value, expected[i].value);
+ if (expected[i].type) {
+ do_check_eq(tokens[i].type, expected[i].type);
+ }
+ }
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_attribute-parsing-02.js b/devtools/client/shared/test/unit/test_attribute-parsing-02.js
new file mode 100644
index 000000000..2c24d8f05
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_attribute-parsing-02.js
@@ -0,0 +1,134 @@
+/* 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} = Components.utils.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 (let {tagName, namespaceURI, attributeName,
+ otherAttributes, attributeValue, expected} of TEST_DATA) {
+ do_print("Testing <" + tagName + " " + attributeName + "='" + attributeValue + "'>");
+
+ let attributes = [
+ ...otherAttributes || [],
+ { name: attributeName, value: attributeValue }
+ ];
+ let tokens = parseAttribute(namespaceURI, tagName, attributes, attributeName);
+ if (!expected) {
+ do_check_true(!tokens);
+ continue;
+ }
+
+ do_print("Checking that the number of parsed tokens is correct");
+ do_check_eq(tokens.length, expected.length);
+
+ for (let i = 0; i < tokens.length; i++) {
+ do_print("Checking the data in token " + i);
+ do_check_eq(tokens[i].value, expected[i].value);
+ do_check_eq(tokens[i].type, expected[i].type);
+ }
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_bezierCanvas.js b/devtools/client/shared/test/unit/test_bezierCanvas.js
new file mode 100644
index 000000000..1decceebb
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_bezierCanvas.js
@@ -0,0 +1,117 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 Cu = Components.utils;
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var {CubicBezier, BezierCanvas} = require("devtools/client/shared/widgets/CubicBezierWidget");
+
+function run_test() {
+ offsetsGetterReturnsData();
+ convertsOffsetsToCoordinates();
+ plotsCanvas();
+}
+
+function offsetsGetterReturnsData() {
+ do_print("offsets getter returns an array of 2 offset objects");
+
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+ let offsets = b.offsets;
+
+ do_check_eq(offsets.length, 2);
+
+ do_check_true("top" in offsets[0]);
+ do_check_true("left" in offsets[0]);
+ do_check_true("top" in offsets[1]);
+ do_check_true("left" in offsets[1]);
+
+ do_check_eq(offsets[0].top, "300px");
+ do_check_eq(offsets[0].left, "0px");
+ do_check_eq(offsets[1].top, "100px");
+ do_check_eq(offsets[1].left, "200px");
+
+ do_print("offsets getter returns data according to current padding");
+
+ b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [0, 0]);
+ offsets = b.offsets;
+
+ do_check_eq(offsets[0].top, "400px");
+ do_check_eq(offsets[0].left, "0px");
+ do_check_eq(offsets[1].top, "0px");
+ do_check_eq(offsets[1].left, "200px");
+}
+
+function convertsOffsetsToCoordinates() {
+ do_print("Converts offsets to coordinates");
+
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+
+ let coordinates = b.offsetsToCoordinates({style: {
+ left: "0px",
+ top: "0px"
+ }});
+ do_check_eq(coordinates.length, 2);
+ do_check_eq(coordinates[0], 0);
+ do_check_eq(coordinates[1], 1.5);
+
+ coordinates = b.offsetsToCoordinates({style: {
+ left: "0px",
+ top: "300px"
+ }});
+ do_check_eq(coordinates[0], 0);
+ do_check_eq(coordinates[1], 0);
+
+ coordinates = b.offsetsToCoordinates({style: {
+ left: "200px",
+ top: "100px"
+ }});
+ do_check_eq(coordinates[0], 1);
+ do_check_eq(coordinates[1], 1);
+}
+
+function plotsCanvas() {
+ do_print("Plots the curve to the canvas");
+
+ let hasDrawnCurve = false;
+ let b = new BezierCanvas(getCanvasMock(), getCubicBezier(), [.25, 0]);
+ b.ctx.bezierCurveTo = () => {
+ hasDrawnCurve = true;
+ };
+ b.plot();
+
+ do_check_true(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/unit/test_cssAngle.js b/devtools/client/shared/test/unit/test_cssAngle.js
new file mode 100644
index 000000000..ecb93bc8f
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssAngle.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test classifyAngle.
+
+"use strict";
+
+var Cu = Components.utils;
+var {require} = Cu.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 (let test of CLASSIFY_TESTS) {
+ let result = angleUtils.classifyAngle(test.input);
+ equal(result, test.output, "test classifyAngle(" + test.input + ")");
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_cssColor-01.js b/devtools/client/shared/test/unit/test_cssColor-01.js
new file mode 100644
index 000000000..13b9b5fa0
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColor-01.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test classifyColor.
+
+"use strict";
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {colorUtils} = require("devtools/shared/css/color");
+
+loader.lazyGetter(this, "DOMUtils", function () {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
+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: "#f0c", output: "hex" },
+ { input: "#f0c0", 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 compareWithDomutils(input, isColor) {
+ let ours = colorUtils.colorToRGBA(input);
+ let platform = DOMUtils.colorToRGBA(input);
+ deepEqual(ours, platform, "color " + input + " matches DOMUtils");
+ if (isColor) {
+ ok(ours !== null, "'" + input + "' is a color");
+ } else {
+ ok(ours === null, "'" + input + "' is not a color");
+ }
+}
+
+function run_test() {
+ for (let test of CLASSIFY_TESTS) {
+ let result = colorUtils.classifyColor(test.input);
+ equal(result, test.output, "test classifyColor(" + test.input + ")");
+
+ let obj = new colorUtils.CssColor("purple");
+ obj.setAuthoredUnitFromColor(test.input);
+ equal(obj.colorUnit, test.output,
+ "test setAuthoredUnitFromColor(" + test.input + ")");
+
+ // Check that our implementation matches DOMUtils.
+ compareWithDomutils(test.input, true);
+
+ // And check some obvious errors.
+ compareWithDomutils("mumble" + test.input, false);
+ compareWithDomutils(test.input + "trailingstuff", false);
+ }
+
+ // Regression test for bug 1303826.
+ let black = new colorUtils.CssColor("#000");
+ black.colorUnit = "name";
+ equal(black.toString(), "black", "test non-upper-case color cycling");
+
+ let 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/unit/test_cssColor-02.js b/devtools/client/shared/test/unit/test_cssColor-02.js
new file mode 100644
index 000000000..c6a039028
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColor-02.js
@@ -0,0 +1,45 @@
+/* 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} = Components.utils.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/unit/test_cssColor-03.js b/devtools/client/shared/test/unit/test_cssColor-03.js
new file mode 100644
index 000000000..c3ef5a5c2
--- /dev/null
+++ b/devtools/client/shared/test/unit/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 Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require, loader} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {colorUtils} = require("devtools/shared/css/color");
+
+loader.lazyGetter(this, "DOMUtils", function () {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
+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 (let test of OLD_STYLE_TESTS) {
+ let ours = colorUtils.colorToRGBA(test, true);
+ let platform = DOMUtils.colorToRGBA(test);
+ deepEqual(ours, platform, "color " + test + " matches DOMUtils");
+ ok(ours !== null, "'" + test + "' is a color");
+ }
+
+ for (let test of CSS_COLOR_4_TESTS) {
+ let oursOld = colorUtils.colorToRGBA(test, true);
+ let oursNew = colorUtils.colorToRGBA(test, false);
+ let platform = DOMUtils.colorToRGBA(test);
+ notEqual(oursOld, platform, "old style parser for color " + test +
+ " should not match DOMUtils");
+ ok(oursOld === null, "'" + test + "' is not a color with old parser");
+ deepEqual(oursNew, platform, `css-color-4 parser for color ${test} matches DOMUtils`);
+ ok(oursNew !== null, "'" + test + "' is a color with css-color-4 parser");
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_cssColorDatabase.js b/devtools/client/shared/test/unit/test_cssColorDatabase.js
new file mode 100644
index 000000000..eb6363ba4
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColorDatabase.js
@@ -0,0 +1,63 @@
+/* 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 Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+const DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+
+const {colorUtils} = require("devtools/shared/css/color");
+const {cssColors} = require("devtools/shared/css/color-db");
+
+function isValid(colorName) {
+ ok(colorUtils.isValidCSSColor(colorName),
+ colorName + " is valid in database");
+ ok(DOMUtils.isValidCSSColor(colorName),
+ colorName + " is valid in DOMUtils");
+}
+
+function checkOne(colorName, checkName) {
+ let ours = colorUtils.colorToRGBA(colorName);
+ let fromDom = DOMUtils.colorToRGBA(colorName);
+ deepEqual(ours, fromDom, colorName + " agrees with DOMUtils");
+
+ isValid(colorName);
+
+ if (checkName) {
+ let {r, g, b} = ours;
+
+ // The color we got might not map back to the same name; but our
+ // implementation should agree with DOMUtils about which name is
+ // canonical.
+ let ourName = colorUtils.rgbToColorName(r, g, b);
+ let domName = DOMUtils.rgbToColorName(r, g, b);
+
+ equal(ourName, domName,
+ colorName + " canonical name agrees with DOMUtils");
+ }
+}
+
+function run_test() {
+ for (let name in cssColors) {
+ checkOne(name, true);
+ }
+ checkOne("transparent", false);
+
+ // Now check that platform didn't add a new name when we weren't
+ // looking.
+ let names = DOMUtils.getCSSValuesForProperty("background-color");
+ for (let 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/unit/test_cubicBezier.js b/devtools/client/shared/test/unit/test_cubicBezier.js
new file mode 100644
index 000000000..9ed6c4eb1
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cubicBezier.js
@@ -0,0 +1,146 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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 Cu = Components.utils;
+var {require} = Cu.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() {
+ do_print("Converts string coordinates to numbers");
+ let c = new CubicBezier(["0", "1", ".5", "-2"]);
+
+ do_check_eq(c.coordinates[0], 0);
+ do_check_eq(c.coordinates[1], 1);
+ do_check_eq(c.coordinates[2], .5);
+ do_check_eq(c.coordinates[3], -2);
+}
+
+function coordinatesToStringOutputsAString() {
+ do_print("coordinates.toString() outputs a string representation");
+
+ let c = new CubicBezier(["0", "1", "0.5", "-2"]);
+ let string = c.coordinates.toString();
+ do_check_eq(string, "0,1,.5,-2");
+
+ c = new CubicBezier([1, 1, 1, 1]);
+ string = c.coordinates.toString();
+ do_check_eq(string, "1,1,1,1");
+}
+
+function pointGettersReturnPointCoordinatesArrays() {
+ do_print("Points getters return arrays of coordinates");
+
+ let c = new CubicBezier([0, .2, .5, 1]);
+ do_check_eq(c.P1[0], 0);
+ do_check_eq(c.P1[1], .2);
+ do_check_eq(c.P2[0], .5);
+ do_check_eq(c.P2[1], 1);
+}
+
+function toStringOutputsCubicBezierValue() {
+ do_print("toString() outputs the cubic-bezier() value");
+
+ let c = new CubicBezier([0, 1, 1, 0]);
+ do_check_eq(c.toString(), "cubic-bezier(0,1,1,0)");
+}
+
+function toStringOutputsCssPresetValues() {
+ do_print("toString() outputs the css predefined values");
+
+ let c = new CubicBezier([0, 0, 1, 1]);
+ do_check_eq(c.toString(), "linear");
+
+ c = new CubicBezier([0.25, 0.1, 0.25, 1]);
+ do_check_eq(c.toString(), "ease");
+
+ c = new CubicBezier([0.42, 0, 1, 1]);
+ do_check_eq(c.toString(), "ease-in");
+
+ c = new CubicBezier([0, 0, 0.58, 1]);
+ do_check_eq(c.toString(), "ease-out");
+
+ c = new CubicBezier([0.42, 0, 0.58, 1]);
+ do_check_eq(c.toString(), "ease-in-out");
+}
+
+function testParseTimingFunction() {
+ do_print("test parseTimingFunction");
+
+ for (let 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, info) {
+ do_print(info);
+
+ let hasThrown = false;
+ try {
+ cb();
+ } catch (e) {
+ hasThrown = true;
+ }
+
+ do_check_true(hasThrown);
+}
diff --git a/devtools/client/shared/test/unit/test_escapeCSSComment.js b/devtools/client/shared/test/unit/test_escapeCSSComment.js
new file mode 100644
index 000000000..19d8a2902
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_escapeCSSComment.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.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 (let test of TEST_DATA) {
+ ++i;
+ do_print("Test #" + i);
+
+ let escaped = escapeCSSComment(test.input);
+ equal(escaped, test.expected);
+ let unescaped = _unescapeCSSComment(escaped);
+ equal(unescaped, test.input);
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_parseDeclarations.js b/devtools/client/shared/test/unit/test_parseDeclarations.js
new file mode 100644
index 000000000..d400a5359
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_parseDeclarations.js
@@ -0,0 +1,439 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const {parseDeclarations, _parseCommentDeclarations} = 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 invalid priority
+ {
+ input: "p1: v1 important;",
+ expected: [
+ {name: "p1", value: "v1 important", priority: "", offsets: [0, 17]}
+ ]
+ },
+ // 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 ignored.
+ {
+ parseComments: true,
+ input: "<!-- color: red; --> color: blue;",
+ expected: [{name: "color", value: "red", priority: "",
+ offsets: [5, 16]},
+ {name: "color", value: "blue", priority: "",
+ offsets: [21, 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]}]
+ },
+];
+
+function run_test() {
+ run_basic_tests();
+ run_comment_tests();
+}
+
+// Test parseDeclarations.
+function run_basic_tests() {
+ for (let test of TEST_DATA) {
+ do_print("Test input string " + test.input);
+ let output;
+ try {
+ output = parseDeclarations(isCssPropertyKnown, test.input,
+ test.parseComments);
+ } catch (e) {
+ do_print("parseDeclarations threw an exception with the given input " +
+ "string");
+ if (test.throws) {
+ do_print("Exception expected");
+ do_check_true(true);
+ } else {
+ do_print("Exception unexpected\n" + e);
+ do_check_true(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 (let test of COMMENT_DATA) {
+ do_print("Test input string " + test.input);
+ let output = _parseCommentDeclarations(isCssPropertyKnown, test.input, 0,
+ test.input.length + 4);
+ deepEqual(output, test.expected);
+ }
+}
+
+function assertOutput(actual, expected) {
+ if (actual.length === expected.length) {
+ for (let i = 0; i < expected.length; i++) {
+ do_check_true(!!actual[i]);
+ do_print("Check that the output item has the expected name, " +
+ "value and priority");
+ do_check_eq(expected[i].name, actual[i].name);
+ do_check_eq(expected[i].value, actual[i].value);
+ do_check_eq(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 (let prop of actual) {
+ do_print("Actual output contained: {name: " + prop.name + ", value: " +
+ prop.value + ", priority: " + prop.priority + "}");
+ }
+ do_check_eq(actual.length, expected.length);
+ }
+}
diff --git a/devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js b/devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js
new file mode 100644
index 000000000..ccd778c4a
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_parsePseudoClassesAndAttributes.js
@@ -0,0 +1,213 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.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 (let 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 (let 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/unit/test_parseSingleValue.js b/devtools/client/shared/test/unit/test_parseSingleValue.js
new file mode 100644
index 000000000..73e4f0ac4
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_parseSingleValue.js
@@ -0,0 +1,93 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.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 (let test of TEST_DATA) {
+ do_print("Test input value " + test.input);
+ try {
+ let output = parseSingleValue(isCssPropertyKnown, test.input);
+ assertOutput(output, test.expected);
+ } catch (e) {
+ do_print("parseSingleValue threw an exception with the given input " +
+ "value");
+ if (test.throws) {
+ do_print("Exception expected");
+ do_check_true(true);
+ } else {
+ do_print("Exception unexpected\n" + e);
+ do_check_true(false);
+ }
+ }
+ }
+}
+
+function assertOutput(actual, expected) {
+ do_print("Check that the output has the expected value and priority");
+ do_check_eq(expected.value, actual.value);
+ do_check_eq(expected.priority, actual.priority);
+}
diff --git a/devtools/client/shared/test/unit/test_rewriteDeclarations.js b/devtools/client/shared/test/unit/test_rewriteDeclarations.js
new file mode 100644
index 000000000..0183ea3c5
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_rewriteDeclarations.js
@@ -0,0 +1,529 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {RuleRewriter} = require("devtools/shared/css/parsing-utils");
+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:#f0c; */\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:#f0c; background-color: seagreen; */\n e:f;",
+ instruction: {type: "remove", name: "color", index: 1},
+ expected: "\n a:b;\n /* background-color: seagreen; */\n e:f;",
+ },
+];
+
+function rewriteDeclarations(inputString, instruction, defaultIndentation) {
+ let 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 (let test of TEST_DATA) {
+ let {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/unit/test_source-utils.js b/devtools/client/shared/test/unit/test_source-utils.js
new file mode 100644
index 000000000..2ff55b92e
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_source-utils.js
@@ -0,0 +1,181 @@
+/* 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 } = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
+const sourceUtils = require("devtools/client/shared/source-utils");
+
+function run_test() {
+ run_next_test();
+}
+
+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"
+];
+
+// Test `sourceUtils.parseURL`
+add_task(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(function* () {
+ for (let url of CHROME_URLS) {
+ ok(!sourceUtils.isContentScheme(url),
+ `${url} correctly identified as not content scheme`);
+ }
+ for (let url of CONTENT_URLS) {
+ ok(sourceUtils.isContentScheme(url), `${url} correctly identified as content scheme`);
+ }
+});
+
+// Test `sourceUtils.isChromeScheme`.
+add_task(function* () {
+ for (let url of CHROME_URLS) {
+ ok(sourceUtils.isChromeScheme(url), `${url} correctly identified as chrome scheme`);
+ }
+ for (let url of CONTENT_URLS) {
+ ok(!sourceUtils.isChromeScheme(url),
+ `${url} correctly identified as not chrome scheme`);
+ }
+});
+
+// Test `sourceUtils.isDataScheme`.
+add_task(function* () {
+ let dataURI = "data:text/html;charset=utf-8,<!DOCTYPE html></html>";
+ ok(sourceUtils.isDataScheme(dataURI), `${dataURI} correctly identified as data scheme`);
+
+ for (let url of CHROME_URLS) {
+ ok(!sourceUtils.isDataScheme(url), `${url} correctly identified as not data scheme`);
+ }
+ for (let url of CONTENT_URLS) {
+ ok(!sourceUtils.isDataScheme(url), `${url} correctly identified as not data scheme`);
+ }
+});
+
+// Test `sourceUtils.getSourceNames`.
+add_task(function* () {
+ testAbbreviation("http://example.com/foo/bar/baz/boo.js",
+ "boo.js",
+ "http://example.com/foo/bar/baz/boo.js",
+ "example.com");
+});
+
+// Test `sourceUtils.isScratchpadTheme`
+add_task(function* () {
+ ok(sourceUtils.isScratchpadScheme("Scratchpad/1"),
+ "Scratchpad/1 identified as scratchpad");
+ ok(sourceUtils.isScratchpadScheme("Scratchpad/20"),
+ "Scratchpad/20 identified as scratchpad");
+ ok(!sourceUtils.isScratchpadScheme("http://www.mozilla.org"), "http://www.mozilla.org not identified as scratchpad");
+});
+
+// Test `sourceUtils.getSourceNames`.
+add_task(function* () {
+ // Check length
+ let 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>");
+
+ let longDataURI = `data:image/png;base64,${new Array(100).fill("a").join("")}`;
+ let 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.
+ let 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(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) {
+ let 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/unit/test_suggestion-picker.js b/devtools/client/shared/test/unit/test_suggestion-picker.js
new file mode 100644
index 000000000..28c9df13b
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_suggestion-picker.js
@@ -0,0 +1,149 @@
+/* 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} = Components.utils.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() {
+ do_print("Running ensureMostRelevantIndexProvidedByHelperFunction()");
+
+ for (let testData of TEST_DATA) {
+ let { items, sortedItems, expectedIndex } = testData;
+ let 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() {
+ do_print("Running ensureMostRelevantIndexProvidedByClassMethod()");
+
+ for (let testData of CSS_TEST_DATA) {
+ let { items, expectedIndex } = testData;
+ let mostRelevantIndex = findMostRelevantCssPropertyIndex(items);
+ strictEqual(mostRelevantIndex, expectedIndex);
+ }
+}
+
+function ensureErrorThrownWithInvalidArguments() {
+ do_print("Running ensureErrorThrownWithInvalidTypeArgument()");
+
+ let expectedError = "Please provide valid items and sortedItems arrays.";
+ // No arguments passed.
+ throws(() => findMostRelevantIndex(), expectedError);
+ // Invalid arguments passed.
+ throws(() => findMostRelevantIndex([]), expectedError);
+ throws(() => findMostRelevantIndex(null, []), expectedError);
+ throws(() => findMostRelevantIndex([], "string"), expectedError);
+ throws(() => findMostRelevantIndex("string", []), expectedError);
+}
diff --git a/devtools/client/shared/test/unit/test_undoStack.js b/devtools/client/shared/test/unit/test_undoStack.js
new file mode 100644
index 000000000..7499614fd
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_undoStack.js
@@ -0,0 +1,98 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {Loader} = Components.utils.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+
+const loader = new Loader.Loader({
+ paths: {
+ "": "resource://gre/modules/commonjs/",
+ "devtools": "resource://devtools",
+ },
+ globals: {},
+});
+const require = Loader.Require(loader, { id: "undo-test" });
+
+const {UndoStack} = require("devtools/client/shared/undo");
+
+const MAX_SIZE = 5;
+
+function run_test() {
+ let str = "";
+ let stack = new UndoStack(MAX_SIZE);
+
+ function add(ch) {
+ stack.do(function () {
+ str += ch;
+ }, function () {
+ str = str.slice(0, -1);
+ });
+ }
+
+ do_check_false(stack.canUndo());
+ do_check_false(stack.canRedo());
+
+ // Check adding up to the limit of the size
+ add("a");
+ do_check_true(stack.canUndo());
+ do_check_false(stack.canRedo());
+
+ add("b");
+ add("c");
+ add("d");
+ add("e");
+
+ do_check_eq(str, "abcde");
+
+ // Check a simple undo+redo
+ stack.undo();
+
+ do_check_eq(str, "abcd");
+ do_check_true(stack.canRedo());
+
+ stack.redo();
+ do_check_eq(str, "abcde");
+ do_check_false(stack.canRedo());
+
+ // Check an undo followed by a new action
+ stack.undo();
+ do_check_eq(str, "abcd");
+
+ add("q");
+ do_check_eq(str, "abcdq");
+ do_check_false(stack.canRedo());
+
+ stack.undo();
+ do_check_eq(str, "abcd");
+ stack.redo();
+ do_check_eq(str, "abcdq");
+
+ // Revert back to the beginning of the queue...
+ while (stack.canUndo()) {
+ stack.undo();
+ }
+ do_check_eq(str, "");
+
+ // Now put it all back....
+ while (stack.canRedo()) {
+ stack.redo();
+ }
+ do_check_eq(str, "abcdq");
+
+ // Now go over the undo limit...
+ add("1");
+ add("2");
+ add("3");
+
+ do_check_eq(str, "abcdq123");
+
+ // And now undoing the whole stack should only undo 5 actions.
+ while (stack.canUndo()) {
+ stack.undo();
+ }
+
+ do_check_eq(str, "abc");
+}
diff --git a/devtools/client/shared/test/unit/xpcshell.ini b/devtools/client/shared/test/unit/xpcshell.ini
new file mode 100644
index 000000000..b3c5791ec
--- /dev/null
+++ b/devtools/client/shared/test/unit/xpcshell.ini
@@ -0,0 +1,30 @@
+[DEFAULT]
+tags = devtools
+head =
+tail =
+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_cssColorDatabase.js]
+[test_cubicBezier.js]
+[test_escapeCSSComment.js]
+[test_parseDeclarations.js]
+[test_parsePseudoClassesAndAttributes.js]
+[test_parseSingleValue.js]
+[test_rewriteDeclarations.js]
+[test_source-utils.js]
+[test_suggestion-picker.js]
+[test_undoStack.js]
+[test_VariablesView_filtering-without-controller.js]
+[test_VariablesView_getString_promise.js]