diff options
Diffstat (limited to 'devtools/client/shared/test/unit')
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] |