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