diff options
Diffstat (limited to 'devtools/shared/tests/unit')
30 files changed, 1840 insertions, 0 deletions
diff --git a/devtools/shared/tests/unit/.eslintrc.js b/devtools/shared/tests/unit/.eslintrc.js new file mode 100644 index 000000000..012428019 --- /dev/null +++ b/devtools/shared/tests/unit/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the common devtools xpcshell eslintrc config. + "extends": "../../../.eslintrc.xpcshell.js" +}; diff --git a/devtools/shared/tests/unit/exposeLoader.js b/devtools/shared/tests/unit/exposeLoader.js new file mode 100644 index 000000000..949640a03 --- /dev/null +++ b/devtools/shared/tests/unit/exposeLoader.js @@ -0,0 +1,8 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +exports.exerciseLazyRequire = (name, path) => { + const o = {}; + loader.lazyRequireGetter(o, name, path); + return o; +}; diff --git a/devtools/shared/tests/unit/head_devtools.js b/devtools/shared/tests/unit/head_devtools.js new file mode 100644 index 000000000..f0f47c93a --- /dev/null +++ b/devtools/shared/tests/unit/head_devtools.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +const {require, DevToolsLoader, devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const flags = require("devtools/shared/flags"); + +flags.testing = true; +do_register_cleanup(() => { + flags.testing = false; +}); + +// Register a console listener, so console messages don't just disappear +// into the ether. + +// If for whatever reason the test needs to post console errors that aren't +// failures, set this to true. +var ALLOW_CONSOLE_ERRORS = false; + +var errorCount = 0; +var listener = { + observe: function (aMessage) { + errorCount++; + try { + // If we've been given an nsIScriptError, then we can print out + // something nicely formatted, for tools like Emacs to pick up. + var scriptError = aMessage.QueryInterface(Ci.nsIScriptError); + dump(aMessage.sourceName + ":" + aMessage.lineNumber + ": " + + scriptErrorFlagsToKind(aMessage.flags) + ": " + + aMessage.errorMessage + "\n"); + var string = aMessage.errorMessage; + } catch (x) { + // Be a little paranoid with message, as the whole goal here is to lose + // no information. + try { + var string = "" + aMessage.message; + } catch (x) { + var string = "<error converting error message to string>"; + } + } + + // Make sure we exit all nested event loops so that the test can finish. + while (DebuggerServer.xpcInspector.eventLoopNestLevel > 0) { + DebuggerServer.xpcInspector.exitNestedEventLoop(); + } + + if (!ALLOW_CONSOLE_ERRORS) { + do_throw("head_devtools.js got console message: " + string + "\n"); + } + } +}; + +var consoleService = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService); +consoleService.registerListener(listener); diff --git a/devtools/shared/tests/unit/test_assert.js b/devtools/shared/tests/unit/test_assert.js new file mode 100644 index 000000000..b87171751 --- /dev/null +++ b/devtools/shared/tests/unit/test_assert.js @@ -0,0 +1,36 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test DevToolsUtils.assert + +ALLOW_CONSOLE_ERRORS = true; + +function run_test() { + // Enable assertions. + flags.testing = true; + + const { assert } = DevToolsUtils; + equal(typeof assert, "function"); + + try { + assert(true, "this assertion should not fail"); + } catch (e) { + // If you catch assertion failures in practice, I will hunt you down. I get + // email notifications every time it happens. + ok(false, "Should not get an error for an assertion that should not fail. Got " + + DevToolsUtils.safeErrorString(e)); + } + + let assertionFailed = false; + try { + assert(false, "this assertion should fail"); + } catch (e) { + ok(e.message.startsWith("Assertion failure:"), + "Should be an assertion failure error"); + assertionFailed = true; + } + + ok(assertionFailed, + "The assertion should have failed, which should throw an error when assertions are enabled."); +} diff --git a/devtools/shared/tests/unit/test_async-utils.js b/devtools/shared/tests/unit/test_async-utils.js new file mode 100644 index 000000000..2b09b8260 --- /dev/null +++ b/devtools/shared/tests/unit/test_async-utils.js @@ -0,0 +1,157 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test async-utils.js + +const {Task} = require("devtools/shared/task"); +// |const| will not work because +// it will make the Promise object immutable before assigning. +// Using Object.defineProperty() instead. +Object.defineProperty(this, "Promise", { + value: require("promise"), + writable: false, configurable: false +}); +const {asyncOnce, promiseInvoke, promiseCall} = require("devtools/shared/async-utils"); + +function run_test() { + do_test_pending(); + Task.spawn(function* () { + yield test_async_args(asyncOnce); + yield test_async_return(asyncOnce); + yield test_async_throw(asyncOnce); + + yield test_async_once(); + yield test_async_invoke(); + do_test_finished(); + }).then(null, error => { + do_throw(error); + }); +} + +// Test that arguments are correctly passed through to the async function. +function test_async_args(async) { + let obj = { + method: async(function* (a, b) { + do_check_eq(this, obj); + do_check_eq(a, "foo"); + do_check_eq(b, "bar"); + }) + }; + + return obj.method("foo", "bar"); +} + +// Test that the return value from the async function is resolution value of +// the promise. +function test_async_return(async) { + let obj = { + method: async(function* (a, b) { + return a + b; + }) + }; + + return obj.method("foo", "bar").then(ret => { + do_check_eq(ret, "foobar"); + }); +} + +// Test that the throwing from an async function rejects the promise. +function test_async_throw(async) { + let obj = { + method: async(function* () { + throw "boom"; + }) + }; + + return obj.method().then(null, error => { + do_check_eq(error, "boom"); + }); +} + +// Test that asyncOnce only runs the async function once per instance and +// returns the same promise for that instance. +function test_async_once() { + let counter = 0; + + function Foo() {} + Foo.prototype = { + ran: false, + method: asyncOnce(function* () { + yield Promise.resolve(); + if (this.ran) { + do_throw("asyncOnce function unexpectedly ran twice on the same object"); + } + this.ran = true; + return counter++; + }) + }; + + let foo1 = new Foo(); + let foo2 = new Foo(); + let p1 = foo1.method(); + let p2 = foo2.method(); + + do_check_neq(p1, p2); + + let p3 = foo1.method(); + do_check_eq(p1, p3); + do_check_false(foo1.ran); + + let p4 = foo2.method(); + do_check_eq(p2, p4); + do_check_false(foo2.ran); + + return p1.then(ret => { + do_check_true(foo1.ran); + do_check_eq(ret, 0); + return p2; + }).then(ret => { + do_check_true(foo2.ran); + do_check_eq(ret, 1); + }); +} + +// Test invoke and call. +function test_async_invoke() { + return Task.spawn(function* () { + function func(a, b, expectedThis, callback) { + "use strict"; + do_check_eq(a, "foo"); + do_check_eq(b, "bar"); + do_check_eq(this, expectedThis); + callback(a + b); + } + + // Test call. + let callResult = yield promiseCall(func, "foo", "bar", undefined); + do_check_eq(callResult, "foobar"); + + + // Test invoke. + let obj = { method: func }; + let invokeResult = yield promiseInvoke(obj, obj.method, "foo", "bar", obj); + do_check_eq(invokeResult, "foobar"); + + + // Test passing multiple values to the callback. + function multipleResults(callback) { + callback("foo", "bar"); + } + + let results = yield promiseCall(multipleResults); + do_check_eq(results.length, 2); + do_check_eq(results[0], "foo"); + do_check_eq(results[1], "bar"); + + + // Test throwing from the function. + function thrower() { + throw "boom"; + } + + yield promiseCall(thrower).then(null, error => { + do_check_eq(error, "boom"); + }); + }); +} diff --git a/devtools/shared/tests/unit/test_console_filtering.js b/devtools/shared/tests/unit/test_console_filtering.js new file mode 100644 index 000000000..431e2b234 --- /dev/null +++ b/devtools/shared/tests/unit/test_console_filtering.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { console, ConsoleAPI } = require("resource://gre/modules/Console.jsm"); +const { ConsoleAPIListener } = require("devtools/server/actors/utils/webconsole-utils"); +const Services = require("Services"); + +var seenMessages = 0; +var seenTypes = 0; + +var callback = { + onConsoleAPICall: function (aMessage) { + if (aMessage.consoleID && aMessage.consoleID == "addon/foo") { + do_check_eq(aMessage.level, "warn"); + do_check_eq(aMessage.arguments[0], "Warning from foo"); + seenTypes |= 1; + } else if (aMessage.originAttributes && + aMessage.originAttributes.addonId == "bar") { + do_check_eq(aMessage.level, "error"); + do_check_eq(aMessage.arguments[0], "Error from bar"); + seenTypes |= 2; + } else { + do_check_eq(aMessage.level, "log"); + do_check_eq(aMessage.arguments[0], "Hello from default console"); + seenTypes |= 4; + } + seenMessages++; + } +}; + +function createFakeAddonWindow({addonId} = {}) { + let baseURI = Services.io.newURI("about:blank", null, null); + let originAttributes = {addonId}; + let principal = Services.scriptSecurityManager + .createCodebasePrincipal(baseURI, originAttributes); + let chromeWebNav = Services.appShell.createWindowlessBrowser(true); + let docShell = chromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell); + docShell.createAboutBlankContentViewer(principal); + let addonWindow = docShell.contentViewer.DOMDocument.defaultView; + + return {addonWindow, chromeWebNav}; +} + +/** + * Tests that the consoleID property of the ConsoleAPI options gets passed + * through to console messages. + */ +function run_test() { + // console1 Test Console.jsm messages tagged by the Addon SDK + // are still filtered correctly. + let console1 = new ConsoleAPI({ + consoleID: "addon/foo" + }); + + // console2 - WebExtension page's console messages tagged + // by 'originAttributes.addonId' are filtered correctly. + let {addonWindow, chromeWebNav} = createFakeAddonWindow({addonId: "bar"}); + let console2 = addonWindow.console; + + // console - Plain console object (messages are tagged with window ids + // and originAttributes, but the addonId will be empty). + console.log("Hello from default console"); + + console1.warn("Warning from foo"); + console2.error("Error from bar"); + + let listener = new ConsoleAPIListener(null, callback); + listener.init(); + let messages = listener.getCachedMessages(); + + seenTypes = 0; + seenMessages = 0; + messages.forEach(callback.onConsoleAPICall); + do_check_eq(seenMessages, 3); + do_check_eq(seenTypes, 7); + + seenTypes = 0; + seenMessages = 0; + console.log("Hello from default console"); + console1.warn("Warning from foo"); + console2.error("Error from bar"); + do_check_eq(seenMessages, 3); + do_check_eq(seenTypes, 7); + + listener.destroy(); + + listener = new ConsoleAPIListener(null, callback, {addonId: "foo"}); + listener.init(); + messages = listener.getCachedMessages(); + + seenTypes = 0; + seenMessages = 0; + messages.forEach(callback.onConsoleAPICall); + do_check_eq(seenMessages, 2); + do_check_eq(seenTypes, 1); + + seenTypes = 0; + seenMessages = 0; + console.log("Hello from default console"); + console1.warn("Warning from foo"); + console2.error("Error from bar"); + do_check_eq(seenMessages, 1); + do_check_eq(seenTypes, 1); + + listener.destroy(); + + listener = new ConsoleAPIListener(null, callback, {addonId: "bar"}); + listener.init(); + messages = listener.getCachedMessages(); + + seenTypes = 0; + seenMessages = 0; + messages.forEach(callback.onConsoleAPICall); + do_check_eq(seenMessages, 3); + do_check_eq(seenTypes, 2); + + seenTypes = 0; + seenMessages = 0; + console.log("Hello from default console"); + console1.warn("Warning from foo"); + console2.error("Error from bar"); + + do_check_eq(seenMessages, 1); + do_check_eq(seenTypes, 2); + + listener.destroy(); + + // Close the addon window's chromeWebNav. + chromeWebNav.close(); +} diff --git a/devtools/shared/tests/unit/test_css-properties-db.js b/devtools/shared/tests/unit/test_css-properties-db.js new file mode 100644 index 000000000..108650a3e --- /dev/null +++ b/devtools/shared/tests/unit/test_css-properties-db.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that the devtool's client-side CSS properties database is in sync with the values + * on the platform (in Nightly only). If they are not, then `mach devtools-css-db` needs + * to be run to make everything up to date. Nightly, aurora, beta, and release may have + * different CSS properties and values. These are based on preferences and compiler flags. + * + * This test broke uplifts as the database needed to be regenerated every uplift. The + * combination of compiler flags and preferences means that it's too difficult to + * statically determine which properties are enabled between Firefox releases. + * + * Because of these difficulties, the database only needs to be up to date with Nightly. + * It is a fallback that is only used if the remote debugging protocol doesn't support + * providing a CSS database, so it's ok if the provided properties don't exactly match + * the inspected target in this particular case. + */ + +"use strict"; + +const DOMUtils = Components.classes["@mozilla.org/inspector/dom-utils;1"] + .getService(Components.interfaces.inIDOMUtils); + +const {PSEUDO_ELEMENTS, CSS_PROPERTIES} = require("devtools/shared/css/generated/properties-db"); +const {generateCssProperties} = require("devtools/server/actors/css-properties"); + +function run_test() { + const propertiesErrorMessage = "If this assertion fails, then the client side CSS " + + "properties list in devtools is out of sync with the " + + "CSS properties on the platform. To fix this " + + "assertion run `mach devtools-css-db` to re-generate " + + "the client side properties."; + + // Check that the platform and client match for pseudo elements. + deepEqual(PSEUDO_ELEMENTS, DOMUtils.getCSSPseudoElementNames(), `The pseudo elements ` + + `match on the client and platform. ${propertiesErrorMessage}`); + + /** + * Check that the platform and client match for the details on their CSS properties. + * Enumerate each property to aid in debugging. + */ + const platformProperties = generateCssProperties(); + + for (let propertyName in CSS_PROPERTIES) { + const platformProperty = platformProperties[propertyName]; + const clientProperty = CSS_PROPERTIES[propertyName]; + const deepEqual = isJsonDeepEqual(platformProperty, clientProperty); + + if (deepEqual) { + ok(true, `The static database and platform match for "${propertyName}".`); + } else { + const prefMessage = `The static database and platform do not match ` + + `for "${propertyName}".`; + if (getPreference(propertyName) === false) { + ok(true, `${prefMessage} However, there is a preference for disabling this ` + + `property on the current build.`); + } else { + ok(false, `${prefMessage} ${propertiesErrorMessage}`); + } + } + } + + /** + * Check that the list of properties on the platform and client are the same. If + * they are not, check that there may be preferences that are disabling them on the + * target platform. + */ + const mismatches = getKeyMismatches(platformProperties, CSS_PROPERTIES) + // Filter out OS-specific properties. + .filter(name => name && name.indexOf("-moz-osx-") === -1); + + if (mismatches.length === 0) { + ok(true, "No client and platform CSS property database mismatches were found."); + } + + mismatches.forEach(propertyName => { + ok(false, `The static database and platform do not agree on the property ` + + `"${propertyName}" ${propertiesErrorMessage}`); + }); +} + +/** + * Check JSON-serializable objects for deep equality. + */ +function isJsonDeepEqual(a, b) { + // Handle primitives. + if (a === b) { + return true; + } + + // Handle arrays. + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!isJsonDeepEqual(a[i], b[i])) { + return false; + } + } + return true; + } + + // Handle objects + if (typeof a === "object" && typeof b === "object") { + for (let key in a) { + if (!isJsonDeepEqual(a[key], b[key])) { + return false; + } + } + + return Object.keys(a).length === Object.keys(b).length; + } + + // Not something handled by these cases, therefore not equal. + return false; +} + +/** + * Take the keys of two objects, and return the ones that don't match. + * + * @param {Object} a + * @param {Object} b + * @return {Array} keys + */ +function getKeyMismatches(a, b) { + const aNames = Object.keys(a); + const bNames = Object.keys(b); + const aMismatches = aNames.filter(key => !bNames.includes(key)); + const bMismatches = bNames.filter(key => { + return !aNames.includes(key) && !aMismatches.includes(key); + }); + + return aMismatches.concat(bMismatches); +} diff --git a/devtools/shared/tests/unit/test_csslexer.js b/devtools/shared/tests/unit/test_csslexer.js new file mode 100644 index 000000000..35855640b --- /dev/null +++ b/devtools/shared/tests/unit/test_csslexer.js @@ -0,0 +1,242 @@ +/* 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/. + */ + +// This file is a copy of layout/style/test/test_csslexer.js, modified +// to use both our pure-JS lexer and the DOMUtils lexer for +// cross-checking. + +"use strict"; + +const jsLexer = require("devtools/shared/css/lexer"); +const domutils = Components.classes["@mozilla.org/inspector/dom-utils;1"] + .getService(Components.interfaces.inIDOMUtils); + +// An object that acts like a CSSLexer but verifies that the DOM lexer +// and the JS lexer do the same thing. +function DoubleLexer(input) { + do_print("DoubleLexer input: " + input); + this.domLexer = domutils.getCSSLexer(input); + this.jsLexer = jsLexer.getCSSLexer(input); +} + +DoubleLexer.prototype = { + checkState: function () { + equal(this.domLexer.lineNumber, this.jsLexer.lineNumber, + "check line number"); + equal(this.domLexer.columnNumber, this.jsLexer.columnNumber, + "check column number"); + }, + + get lineNumber() { + return this.domLexer.lineNumber; + }, + + get columnNumber() { + return this.domLexer.columnNumber; + }, + + performEOFFixup: function (inputString, preserveBackslash) { + let d = this.domLexer.performEOFFixup(inputString, preserveBackslash); + let j = this.jsLexer.performEOFFixup(inputString, preserveBackslash); + + equal(d, j); + return d; + }, + + mungeNumber: function (token) { + if (token && (token.tokenType === "number" || + token.tokenType === "percentage") && + !token.isInteger) { + // The JS lexer does its computations in double, but the + // platform lexer does its computations in float. Account for + // this discrepancy in a way that's sufficient for this test. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1163047 + token.number = parseFloat(token.number.toPrecision(8)); + } + }, + + nextToken: function () { + // Check state both before and after. + this.checkState(); + + let d = this.domLexer.nextToken(); + let j = this.jsLexer.nextToken(); + + this.mungeNumber(d); + this.mungeNumber(j); + + deepEqual(d, j); + + this.checkState(); + + return d; + } +}; + +function test_lexer(cssText, tokenTypes) { + let lexer = new DoubleLexer(cssText); + let reconstructed = ""; + let lastTokenEnd = 0; + let i = 0; + while (true) { + let token = lexer.nextToken(); + if (!token) { + break; + } + let combined = token.tokenType; + if (token.text) { + combined += ":" + token.text; + } + equal(combined, tokenTypes[i]); + ok(token.endOffset > token.startOffset); + equal(token.startOffset, lastTokenEnd); + lastTokenEnd = token.endOffset; + reconstructed += cssText.substring(token.startOffset, token.endOffset); + ++i; + } + // Ensure that we saw the correct number of tokens. + equal(i, tokenTypes.length); + // Ensure that the reported offsets cover all the text. + equal(reconstructed, cssText); +} + +var LEX_TESTS = [ + ["simple", ["ident:simple"]], + ["simple: { hi; }", + ["ident:simple", "symbol::", + "whitespace", "symbol:{", + "whitespace", "ident:hi", + "symbol:;", "whitespace", + "symbol:}"]], + ["/* whatever */", ["comment"]], + ["'string'", ["string:string"]], + ['"string"', ["string:string"]], + ["rgb(1,2,3)", ["function:rgb", "number", + "symbol:,", "number", + "symbol:,", "number", + "symbol:)"]], + ["@media", ["at:media"]], + ["#hibob", ["id:hibob"]], + ["#123", ["hash:123"]], + ["23px", ["dimension:px"]], + ["23%", ["percentage"]], + ["url(http://example.com)", ["url:http://example.com"]], + ["url('http://example.com')", ["url:http://example.com"]], + ["url( 'http://example.com' )", + ["url:http://example.com"]], + // In CSS Level 3, this is an ordinary URL, not a BAD_URL. + ["url(http://example.com", ["url:http://example.com"]], + // See bug 1153981 to understand why this gets a SYMBOL token. + ["url(http://example.com @", ["bad_url:http://example.com", "symbol:@"]], + ["quo\\ting", ["ident:quoting"]], + ["'bad string\n", ["bad_string:bad string", "whitespace"]], + ["~=", ["includes"]], + ["|=", ["dashmatch"]], + ["^=", ["beginsmatch"]], + ["$=", ["endsmatch"]], + ["*=", ["containsmatch"]], + + // URANGE may be on the way out, and it isn't used by devutils, so + // let's skip it. + + ["<!-- html comment -->", ["htmlcomment", "whitespace", "ident:html", + "whitespace", "ident:comment", "whitespace", + "htmlcomment"]], + + // earlier versions of CSS had "bad comment" tokens, but in level 3, + // unterminated comments are just comments. + ["/* bad comment", ["comment"]] +]; + +function test_lexer_linecol(cssText, locations) { + let lexer = new DoubleLexer(cssText); + let i = 0; + while (true) { + let token = lexer.nextToken(); + let startLine = lexer.lineNumber; + let startColumn = lexer.columnNumber; + + // We do this in a bit of a funny way so that we can also test the + // location of the EOF. + let combined = ":" + startLine + ":" + startColumn; + if (token) { + combined = token.tokenType + combined; + } + + equal(combined, locations[i]); + ++i; + + if (!token) { + break; + } + } + // Ensure that we saw the correct number of tokens. + equal(i, locations.length); +} + +function test_lexer_eofchar(cssText, argText, expectedAppend, + expectedNoAppend) { + let lexer = new DoubleLexer(cssText); + while (lexer.nextToken()) { + // Nothing. + } + + do_print("EOF char test, input = " + cssText); + + let result = lexer.performEOFFixup(argText, true); + equal(result, expectedAppend); + + result = lexer.performEOFFixup(argText, false); + equal(result, expectedNoAppend); +} + +var LINECOL_TESTS = [ + ["simple", ["ident:0:0", ":0:6"]], + ["\n stuff", ["whitespace:0:0", "ident:1:4", ":1:9"]], + ['"string with \\\nnewline" \r\n', ["string:0:0", "whitespace:1:8", + ":2:0"]] +]; + +var EOFCHAR_TESTS = [ + ["hello", "hello"], + ["hello \\", "hello \\\\", "hello \\\uFFFD"], + ["'hello", "'hello'"], + ["\"hello", "\"hello\""], + ["'hello\\", "'hello\\\\'", "'hello'"], + ["\"hello\\", "\"hello\\\\\"", "\"hello\""], + ["/*hello", "/*hello*/"], + ["/*hello*", "/*hello*/"], + ["/*hello\\", "/*hello\\*/"], + ["url(hello", "url(hello)"], + ["url('hello", "url('hello')"], + ["url(\"hello", "url(\"hello\")"], + ["url(hello\\", "url(hello\\\\)", "url(hello\\\uFFFD)"], + ["url('hello\\", "url('hello\\\\')", "url('hello')"], + ["url(\"hello\\", "url(\"hello\\\\\")", "url(\"hello\")"], +]; + +function run_test() { + let text, result; + for ([text, result] of LEX_TESTS) { + test_lexer(text, result); + } + + for ([text, result] of LINECOL_TESTS) { + test_lexer_linecol(text, result); + } + + let expectedAppend, expectedNoAppend; + for ([text, expectedAppend, expectedNoAppend] of EOFCHAR_TESTS) { + if (!expectedNoAppend) { + expectedNoAppend = expectedAppend; + } + test_lexer_eofchar(text, text, expectedAppend, expectedNoAppend); + } + + // Ensure that passing a different inputString to performEOFFixup + // doesn't cause an assertion trying to strip a backslash from the + // end of an empty string. + test_lexer_eofchar("'\\", "", "\\'", "'"); +} diff --git a/devtools/shared/tests/unit/test_defer.js b/devtools/shared/tests/unit/test_defer.js new file mode 100644 index 000000000..c0babe961 --- /dev/null +++ b/devtools/shared/tests/unit/test_defer.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const defer = require("devtools/shared/defer"); + +function testResolve() { + const deferred = defer(); + deferred.resolve("success"); + return deferred.promise; +} + +function testReject() { + const deferred = defer(); + deferred.reject("error"); + return deferred.promise; +} + +add_task(function* () { + const success = yield testResolve(); + equal(success, "success"); + + let error; + try { + yield testReject(); + } catch (e) { + error = e; + } + + equal(error, "error"); +}); diff --git a/devtools/shared/tests/unit/test_defineLazyPrototypeGetter.js b/devtools/shared/tests/unit/test_defineLazyPrototypeGetter.js new file mode 100644 index 000000000..d729e7473 --- /dev/null +++ b/devtools/shared/tests/unit/test_defineLazyPrototypeGetter.js @@ -0,0 +1,68 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test DevToolsUtils.defineLazyPrototypeGetter + +function Class() {} +DevToolsUtils.defineLazyPrototypeGetter(Class.prototype, "foo", () => []); + + +function run_test() { + test_prototype_attributes(); + test_instance_attributes(); + test_multiple_instances(); + test_callback_receiver(); +} + +function test_prototype_attributes() { + // Check that the prototype has a getter property with expected attributes. + let descriptor = Object.getOwnPropertyDescriptor(Class.prototype, "foo"); + do_check_eq(typeof descriptor.get, "function"); + do_check_eq(descriptor.set, undefined); + do_check_eq(descriptor.enumerable, false); + do_check_eq(descriptor.configurable, true); +} + +function test_instance_attributes() { + // Instances should not have an own property until the lazy getter has been + // activated. + let instance = new Class(); + do_check_false(instance.hasOwnProperty("foo")); + instance.foo; + do_check_true(instance.hasOwnProperty("foo")); + + // Check that the instance has an own property with the expecred value and + // attributes after the lazy getter is activated. + let descriptor = Object.getOwnPropertyDescriptor(instance, "foo"); + do_check_true(descriptor.value instanceof Array); + do_check_eq(descriptor.writable, true); + do_check_eq(descriptor.enumerable, false); + do_check_eq(descriptor.configurable, true); +} + +function test_multiple_instances() { + let instance1 = new Class(); + let instance2 = new Class(); + let foo1 = instance1.foo; + let foo2 = instance2.foo; + // Check that the lazy getter returns the expected type of value. + do_check_true(foo1 instanceof Array); + do_check_true(foo2 instanceof Array); + // Make sure the lazy getter runs once and only once per instance. + do_check_eq(instance1.foo, foo1); + do_check_eq(instance2.foo, foo2); + // Make sure each instance gets its own unique value. + do_check_neq(foo1, foo2); +} + +function test_callback_receiver() { + function Foo() {} + DevToolsUtils.defineLazyPrototypeGetter(Foo.prototype, "foo", function () { + return this; + }); + + // Check that the |this| value in the callback is the instance itself. + let instance = new Foo(); + do_check_eq(instance.foo, instance); +} diff --git a/devtools/shared/tests/unit/test_executeSoon.js b/devtools/shared/tests/unit/test_executeSoon.js new file mode 100644 index 000000000..6154a3e67 --- /dev/null +++ b/devtools/shared/tests/unit/test_executeSoon.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Client request stacks should span the entire process from before making the + * request to handling the reply from the server. The server frames are not + * included, nor can they be in most cases, since the server can be a remote + * device. + */ + +var { executeSoon } = require("devtools/shared/DevToolsUtils"); +var promise = require("promise"); +var defer = require("devtools/shared/defer"); +var Services = require("Services"); + +var asyncStackEnabled = + Services.prefs.getBoolPref("javascript.options.asyncstack"); + +do_register_cleanup(() => { + Services.prefs.setBoolPref("javascript.options.asyncstack", + asyncStackEnabled); +}); + +add_task(function* () { + Services.prefs.setBoolPref("javascript.options.asyncstack", true); + + yield waitForTick(); + + let stack = Components.stack; + while (stack) { + do_print(stack.name); + if (stack.name == "waitForTick") { + // Reached back to outer function before executeSoon + ok(true, "Complete stack"); + return; + } + stack = stack.asyncCaller || stack.caller; + } + ok(false, "Incomplete stack"); +}); + +function waitForTick() { + let deferred = defer(); + executeSoon(deferred.resolve); + return deferred.promise; +} diff --git a/devtools/shared/tests/unit/test_fetch-bom.js b/devtools/shared/tests/unit/test_fetch-bom.js new file mode 100644 index 000000000..7f299b1ce --- /dev/null +++ b/devtools/shared/tests/unit/test_fetch-bom.js @@ -0,0 +1,76 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Tests for DevToolsUtils.fetch BOM detection. + +const CC = Components.Constructor; + +const { HttpServer } = Cu.import("resource://testing-common/httpd.js", {}); +const BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", "setOutputStream"); + +function write8(bos) { + bos.write8(0xef); + bos.write8(0xbb); + bos.write8(0xbf); + bos.write8(0x68); + bos.write8(0xc4); + bos.write8(0xb1); +} + +function write16be(bos) { + bos.write8(0xfe); + bos.write8(0xff); + bos.write8(0x00); + bos.write8(0x68); + bos.write8(0x01); + bos.write8(0x31); +} + +function write16le(bos) { + bos.write8(0xff); + bos.write8(0xfe); + bos.write8(0x68); + bos.write8(0x00); + bos.write8(0x31); + bos.write8(0x01); +} + +function getHandler(writer) { + return function (request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + + let bos = new BinaryOutputStream(response.bodyOutputStream); + writer(bos); + }; +} + +const server = new HttpServer(); +server.registerDirectory("/", do_get_cwd()); +server.registerPathHandler("/u8", getHandler(write8)); +server.registerPathHandler("/u16be", getHandler(write16be)); +server.registerPathHandler("/u16le", getHandler(write16le)); +server.start(-1); + +const port = server.identity.primaryPort; +const serverURL = "http://localhost:" + port; + +do_register_cleanup(() => { + return new Promise(resolve => server.stop(resolve)); +}); + +add_task(function* () { + yield test_one(serverURL + "/u8", "UTF-8"); + yield test_one(serverURL + "/u16be", "UTF-16BE"); + yield test_one(serverURL + "/u16le", "UTF-16LE"); +}); + +function* test_one(url, encoding) { + // Be sure to set the encoding to something that will yield an + // invalid result if BOM sniffing is not done. + yield DevToolsUtils.fetch(url, { charset: "ISO-8859-1" }).then(({content}) => { + do_check_eq(content, "hı", "The content looks correct for " + encoding); + }); +} diff --git a/devtools/shared/tests/unit/test_fetch-chrome.js b/devtools/shared/tests/unit/test_fetch-chrome.js new file mode 100644 index 000000000..441d4c7ba --- /dev/null +++ b/devtools/shared/tests/unit/test_fetch-chrome.js @@ -0,0 +1,31 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Tests for DevToolsUtils.fetch on chrome:// URI's. + +const URL_FOUND = "chrome://devtools-shared/locale/debugger.properties"; +const URL_NOT_FOUND = "chrome://this/is/not/here.js"; + +/** + * Test that non-existent files are handled correctly. + */ +add_task(function* test_missing() { + yield DevToolsUtils.fetch(URL_NOT_FOUND).then(result => { + do_print(result); + ok(false, "fetch resolved unexpectedly for non-existent chrome:// URI"); + }, () => { + ok(true, "fetch rejected as the chrome:// URI was non-existent."); + }); +}); + +/** + * Tests that existing files are handled correctly. + */ +add_task(function* test_normal() { + yield DevToolsUtils.fetch(URL_FOUND).then(result => { + notDeepEqual(result.content, "", + "chrome:// URI seems to be read correctly."); + }); +}); diff --git a/devtools/shared/tests/unit/test_fetch-file.js b/devtools/shared/tests/unit/test_fetch-file.js new file mode 100644 index 000000000..d9d712322 --- /dev/null +++ b/devtools/shared/tests/unit/test_fetch-file.js @@ -0,0 +1,104 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Tests for DevToolsUtils.fetch on file:// URI's. + +const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {}); +const { OS } = Cu.import("resource://gre/modules/osfile.jsm", {}); + +const TEST_CONTENT = "aéd"; + +// The TEST_CONTENT encoded as UTF-8. +const UTF8_TEST_BUFFER = new Uint8Array([0x61, 0xc3, 0xa9, 0x64]); + +// The TEST_CONTENT encoded as ISO 8859-1. +const ISO_8859_1_BUFFER = new Uint8Array([0x61, 0xe9, 0x64]); + +/** + * Tests that URLs with arrows pointing to an actual source are handled properly + * (bug 808960). For example 'resource://gre/modules/XPIProvider.jsm -> + * file://l10n.js' should load 'file://l10n.js'. + */ +add_task(function* test_arrow_urls() { + let { path } = createTemporaryFile(".js"); + let url = "resource://gre/modules/XPIProvider.jsm -> file://" + path; + + yield OS.File.writeAtomic(path, TEST_CONTENT, { encoding: "utf-8" }); + let { content } = yield DevToolsUtils.fetch(url); + + deepEqual(content, TEST_CONTENT, "The file contents were correctly read."); +}); + +/** + * Tests that empty files are read correctly. + */ +add_task(function* test_empty() { + let { path } = createTemporaryFile(); + let { content } = yield DevToolsUtils.fetch("file://" + path); + deepEqual(content, "", "The empty file was read correctly."); +}); + +/** + * Tests that UTF-8 encoded files are correctly read. + */ +add_task(function* test_encoding_utf8() { + let { path } = createTemporaryFile(); + yield OS.File.writeAtomic(path, UTF8_TEST_BUFFER); + + let { content } = yield DevToolsUtils.fetch(path); + deepEqual(content, TEST_CONTENT, + "The UTF-8 encoded file was correctly read."); +}); + +/** + * Tests that ISO 8859-1 (Latin-1) encoded files are correctly read. + */ +add_task(function* test_encoding_iso_8859_1() { + let { path } = createTemporaryFile(); + yield OS.File.writeAtomic(path, ISO_8859_1_BUFFER); + + let { content } = yield DevToolsUtils.fetch(path); + deepEqual(content, TEST_CONTENT, + "The ISO 8859-1 encoded file was correctly read."); +}); + +/** + * Test that non-existent files are handled correctly. + */ +add_task(function* test_missing() { + yield DevToolsUtils.fetch("file:///file/not/found.right").then(result => { + do_print(result); + ok(false, "Fetch resolved unexpectedly when the file was not found."); + }, () => { + ok(true, "Fetch rejected as expected because the file was not found."); + }); +}); + +/** + * Test that URLs without file:// scheme work. + */ +add_task(function* test_schemeless_files() { + let { path } = createTemporaryFile(); + + yield OS.File.writeAtomic(path, TEST_CONTENT, { encoding: "utf-8" }); + + let { content } = yield DevToolsUtils.fetch(path); + deepEqual(content, TEST_CONTENT, "The content was correct."); +}); + +/** + * Creates a temporary file that is removed after the test completes. + */ +function createTemporaryFile(extension) { + let name = "test_fetch-file-" + Math.random() + (extension || ""); + let file = FileUtils.getFile("TmpD", [name]); + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0755", 8)); + + do_register_cleanup(() => { + file.remove(false); + }); + + return file; +} diff --git a/devtools/shared/tests/unit/test_fetch-http.js b/devtools/shared/tests/unit/test_fetch-http.js new file mode 100644 index 000000000..028fa33ee --- /dev/null +++ b/devtools/shared/tests/unit/test_fetch-http.js @@ -0,0 +1,61 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Tests for DevToolsUtils.fetch on http:// URI's. + +const { HttpServer } = Cu.import("resource://testing-common/httpd.js", {}); + +const server = new HttpServer(); +server.registerDirectory("/", do_get_cwd()); +server.registerPathHandler("/cached.json", cacheRequestHandler); +server.start(-1); + +const port = server.identity.primaryPort; +const serverURL = "http://localhost:" + port; +const CACHED_URL = serverURL + "/cached.json"; +const NORMAL_URL = serverURL + "/test_fetch-http.js"; + +function cacheRequestHandler(request, response) { + do_print("Got request for " + request.path); + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json", false); + + let body = "[" + Math.random() + "]"; + response.bodyOutputStream.write(body, body.length); +} + +do_register_cleanup(() => { + return new Promise(resolve => server.stop(resolve)); +}); + +add_task(function* test_normal() { + yield DevToolsUtils.fetch(NORMAL_URL).then(({content}) => { + ok(content.includes("The content looks correct."), + "The content looks correct."); + }); +}); + +add_task(function* test_caching() { + let initialContent = null; + + do_print("Performing the first request."); + yield DevToolsUtils.fetch(CACHED_URL).then(({content}) => { + do_print("Got the first response: " + content); + initialContent = content; + }); + + do_print("Performing another request, expecting to get cached response."); + yield DevToolsUtils.fetch(CACHED_URL).then(({content}) => { + deepEqual(content, initialContent, "The content was loaded from cache."); + }); + + do_print("Performing a third request with cache bypassed."); + let opts = { loadFromCache: false }; + yield DevToolsUtils.fetch(CACHED_URL, opts).then(({content}) => { + notDeepEqual(content, initialContent, + "The URL wasn't loaded from cache with loadFromCache: false."); + }); +}); diff --git a/devtools/shared/tests/unit/test_fetch-resource.js b/devtools/shared/tests/unit/test_fetch-resource.js new file mode 100644 index 000000000..45e79cfa1 --- /dev/null +++ b/devtools/shared/tests/unit/test_fetch-resource.js @@ -0,0 +1,31 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Tests for DevToolsUtils.fetch on resource:// URI's. + +const URL_FOUND = "resource://devtools/shared/DevToolsUtils.js"; +const URL_NOT_FOUND = "resource://devtools/this/is/not/here.js"; + +/** + * Test that non-existent files are handled correctly. + */ +add_task(function* test_missing() { + yield DevToolsUtils.fetch(URL_NOT_FOUND).then(result => { + do_print(result); + ok(false, "fetch resolved unexpectedly for non-existent resource:// URI"); + }, () => { + ok(true, "fetch rejected as the resource:// URI was non-existent."); + }); +}); + +/** + * Tests that existing files are handled correctly. + */ +add_task(function* test_normal() { + yield DevToolsUtils.fetch(URL_FOUND).then(result => { + notDeepEqual(result.content, "", + "resource:// URI seems to be read correctly."); + }); +}); diff --git a/devtools/shared/tests/unit/test_flatten.js b/devtools/shared/tests/unit/test_flatten.js new file mode 100644 index 000000000..f5a186770 --- /dev/null +++ b/devtools/shared/tests/unit/test_flatten.js @@ -0,0 +1,24 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test ThreadSafeDevToolsUtils.flatten + +function run_test() { + const { flatten } = DevToolsUtils; + + const flat = flatten([["a", "b", "c"], + ["d", "e", "f"], + ["g", "h", "i"]]); + + equal(flat.length, 9); + equal(flat[0], "a"); + equal(flat[1], "b"); + equal(flat[2], "c"); + equal(flat[3], "d"); + equal(flat[4], "e"); + equal(flat[5], "f"); + equal(flat[6], "g"); + equal(flat[7], "h"); + equal(flat[8], "i"); +} diff --git a/devtools/shared/tests/unit/test_indentation.js b/devtools/shared/tests/unit/test_indentation.js new file mode 100644 index 000000000..f7e4403d4 --- /dev/null +++ b/devtools/shared/tests/unit/test_indentation.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const Services = require("Services"); +const { + EXPAND_TAB, + TAB_SIZE, + DETECT_INDENT, + getIndentationFromPrefs, + getIndentationFromIteration, + getIndentationFromString, +} = require("devtools/shared/indentation"); + +function test_indent_from_prefs() { + Services.prefs.setBoolPref(DETECT_INDENT, true); + equal(getIndentationFromPrefs(), false, + "getIndentationFromPrefs returning false"); + + Services.prefs.setIntPref(TAB_SIZE, 73); + Services.prefs.setBoolPref(EXPAND_TAB, false); + Services.prefs.setBoolPref(DETECT_INDENT, false); + deepEqual(getIndentationFromPrefs(), {indentUnit: 73, indentWithTabs: true}, + "getIndentationFromPrefs basic test"); +} + +const TESTS = [ + { + desc: "two spaces", + input: [ + "/*", + " * tricky comment block", + " */", + "div {", + " color: red;", + " background: blue;", + "}", + " ", + "span {", + " padding-left: 10px;", + "}" + ], + expected: {indentUnit: 2, indentWithTabs: false} + }, + { + desc: "four spaces", + input: [ + "var obj = {", + " addNumbers: function() {", + " var x = 5;", + " var y = 18;", + " return x + y;", + " },", + " ", + " /*", + " * Do some stuff to two numbers", + " * ", + " * @param x", + " * @param y", + " * ", + " * @return the result of doing stuff", + " */", + " subtractNumbers: function(x, y) {", + " var x += 7;", + " var y += 18;", + " var result = x - y;", + " result %= 2;", + " }", + "}" + ], + expected: {indentUnit: 4, indentWithTabs: false} + }, + { + desc: "tabs", + input: [ + "/*", + " * tricky comment block", + " */", + "div {", + "\tcolor: red;", + "\tbackground: blue;", + "}", + "", + "span {", + "\tpadding-left: 10px;", + "}" + ], + expected: {indentUnit: 2, indentWithTabs: true} + }, + { + desc: "no indent", + input: [ + "var x = 0;", + " // stray thing", + "var y = 9;", + " ", + "" + ], + expected: {indentUnit: 2, indentWithTabs: false} + }, +]; + +function test_indent_detection() { + Services.prefs.setIntPref(TAB_SIZE, 2); + Services.prefs.setBoolPref(EXPAND_TAB, true); + Services.prefs.setBoolPref(DETECT_INDENT, true); + + for (let test of TESTS) { + let iterFn = function (start, end, callback) { + test.input.slice(start, end).forEach(callback); + }; + + deepEqual(getIndentationFromIteration(iterFn), test.expected, + "test getIndentationFromIteration " + test.desc); + } + + for (let test of TESTS) { + deepEqual(getIndentationFromString(test.input.join("\n")), test.expected, + "test getIndentationFromString " + test.desc); + } +} + +function run_test() { + try { + test_indent_from_prefs(); + test_indent_detection(); + } finally { + Services.prefs.clearUserPref(TAB_SIZE); + Services.prefs.clearUserPref(EXPAND_TAB); + Services.prefs.clearUserPref(DETECT_INDENT); + } +} diff --git a/devtools/shared/tests/unit/test_independent_loaders.js b/devtools/shared/tests/unit/test_independent_loaders.js new file mode 100644 index 000000000..20abed27d --- /dev/null +++ b/devtools/shared/tests/unit/test_independent_loaders.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Ensure that each instance of the Dev Tools loader contains its own loader + * instance, and also returns unique objects. This ensures there is no sharing + * in place between loaders. + */ +function run_test() { + let loader1 = new DevToolsLoader(); + let loader2 = new DevToolsLoader(); + + let indent1 = loader1.require("devtools/shared/indentation"); + let indent2 = loader2.require("devtools/shared/indentation"); + + do_check_true(indent1 !== indent2); + + do_check_true(loader1._provider !== loader2._provider); + do_check_true(loader1._provider.loader !== loader2._provider.loader); +} diff --git a/devtools/shared/tests/unit/test_invisible_loader.js b/devtools/shared/tests/unit/test_invisible_loader.js new file mode 100644 index 000000000..96a68abc4 --- /dev/null +++ b/devtools/shared/tests/unit/test_invisible_loader.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { addDebuggerToGlobal } = Cu.import("resource://gre/modules/jsdebugger.jsm", {}); +addDebuggerToGlobal(this); + +/** + * Ensure that sandboxes created via the Dev Tools loader respect the + * invisibleToDebugger flag. + */ +function run_test() { + visible_loader(); + invisible_loader(); +} + +function visible_loader() { + let loader = new DevToolsLoader(); + loader.invisibleToDebugger = false; + loader.require("devtools/shared/indentation"); + + let dbg = new Debugger(); + let sandbox = loader._provider.loader.sharedGlobalSandbox; + + try { + dbg.addDebuggee(sandbox); + do_check_true(true); + } catch (e) { + do_throw("debugger could not add visible value"); + } + + // Check that for common loader used for tabs, promise modules is Promise.jsm + // Which is required to support unhandled promises rejection in mochitests + const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; + do_check_eq(loader.require("promise"), promise); +} + +function invisible_loader() { + let loader = new DevToolsLoader(); + loader.invisibleToDebugger = true; + loader.require("devtools/shared/indentation"); + + let dbg = new Debugger(); + let sandbox = loader._provider.loader.sharedGlobalSandbox; + + try { + dbg.addDebuggee(sandbox); + do_throw("debugger added invisible value"); + } catch (e) { + do_check_true(true); + } + + // But for browser toolbox loader, promise is loaded as a regular modules out + // of Promise-backend.js, that to be invisible to the debugger and not step + // into it. + const promise = loader.require("promise"); + const promiseModule = loader._provider.loader.modules["resource://gre/modules/Promise-backend.js"]; + do_check_eq(promise, promiseModule.exports); +} diff --git a/devtools/shared/tests/unit/test_isSet.js b/devtools/shared/tests/unit/test_isSet.js new file mode 100644 index 000000000..f85d6ae3c --- /dev/null +++ b/devtools/shared/tests/unit/test_isSet.js @@ -0,0 +1,25 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test ThreadSafeDevToolsUtils.isSet + +function run_test() { + const { isSet } = DevToolsUtils; + + equal(isSet(new Set()), true); + equal(isSet(new Map()), false); + equal(isSet({}), false); + equal(isSet("I swear I'm a Set"), false); + equal(isSet(5), false); + + const systemPrincipal = Cc["@mozilla.org/systemprincipal;1"] + .createInstance(Ci.nsIPrincipal); + const sandbox = new Cu.Sandbox(systemPrincipal); + + equal(isSet(Cu.evalInSandbox("new Set()", sandbox)), true); + equal(isSet(Cu.evalInSandbox("new Map()", sandbox)), false); + equal(isSet(Cu.evalInSandbox("({})", sandbox)), false); + equal(isSet(Cu.evalInSandbox("'I swear I\\'m a Set'", sandbox)), false); + equal(isSet(Cu.evalInSandbox("5", sandbox)), false); +} diff --git a/devtools/shared/tests/unit/test_pluralForm-english.js b/devtools/shared/tests/unit/test_pluralForm-english.js new file mode 100644 index 000000000..67cd3b712 --- /dev/null +++ b/devtools/shared/tests/unit/test_pluralForm-english.js @@ -0,0 +1,29 @@ +/* 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"; + +/** + * This unit test makes sure the plural form for Irish Gaeilge is working by + * using the makeGetter method instead of using the default language (by + * development), English. + */ + +const {PluralForm} = require("devtools/shared/plural-form"); + +function run_test() { + // English has 2 plural forms + do_check_eq(2, PluralForm.numForms()); + + // Make sure for good inputs, things work as expected + for (let num = 0; num <= 200; num++) { + do_check_eq(num == 1 ? "word" : "words", PluralForm.get(num, "word;words")); + } + + // Not having enough plural forms defaults to the first form + do_check_eq("word", PluralForm.get(2, "word")); + + // Empty forms defaults to the first form + do_check_eq("word", PluralForm.get(2, "word;")); +} diff --git a/devtools/shared/tests/unit/test_pluralForm-makeGetter.js b/devtools/shared/tests/unit/test_pluralForm-makeGetter.js new file mode 100644 index 000000000..a9d4928b1 --- /dev/null +++ b/devtools/shared/tests/unit/test_pluralForm-makeGetter.js @@ -0,0 +1,38 @@ +/* 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"; + +/** + * This unit test makes sure the plural form for Irish Gaeilge is working by + * using the makeGetter method instead of using the default language (by + * development), English. + */ + +const {PluralForm} = require("devtools/shared/plural-form"); + +function run_test() { + // Irish is plural rule #11 + let [get, numForms] = PluralForm.makeGetter(11); + + // Irish has 5 plural forms + do_check_eq(5, numForms()); + + // I don't really know Irish, so I'll stick in some dummy text + let words = "is 1;is 2;is 3-6;is 7-10;everything else"; + + let test = function (text, low, high) { + for (let num = low; num <= high; num++) { + do_check_eq(text, get(num, words)); + } + }; + + // Make sure for good inputs, things work as expected + test("everything else", 0, 0); + test("is 1", 1, 1); + test("is 2", 2, 2); + test("is 3-6", 3, 6); + test("is 7-10", 7, 10); + test("everything else", 11, 200); +} diff --git a/devtools/shared/tests/unit/test_prettifyCSS.js b/devtools/shared/tests/unit/test_prettifyCSS.js new file mode 100644 index 000000000..8415c9b83 --- /dev/null +++ b/devtools/shared/tests/unit/test_prettifyCSS.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test prettifyCSS. + +"use strict"; + +const {prettifyCSS} = require("devtools/shared/inspector/css-logic"); + +const TESTS = [ + { name: "simple test", + input: "div { font-family:'Arial Black', Arial, sans-serif; }", + expected: [ + "div {", + "\tfont-family:'Arial Black', Arial, sans-serif;", + "}" + ] + }, + + { name: "whitespace before open brace", + input: "div{}", + expected: [ + "div {", + "}" + ] + }, + + { name: "minified with trailing newline", + input: "\nbody{background:white;}div{font-size:4em;color:red}span{color:green;}\n", + expected: [ + "body {", + "\tbackground:white;", + "}", + "div {", + "\tfont-size:4em;", + "\tcolor:red", + "}", + "span {", + "\tcolor:green;", + "}" + ] + }, + + { name: "leading whitespace", + input: "\n div{color: red;}", + expected: [ + "div {", + "\tcolor: red;", + "}" + ] + }, +]; + +function run_test() { + // Note that prettifyCSS.LINE_SEPARATOR is computed lazily, so we + // ensure it is set. + prettifyCSS(""); + + for (let test of TESTS) { + do_print(test.name); + + let input = test.input.split("\n").join(prettifyCSS.LINE_SEPARATOR); + let output = prettifyCSS(input); + let expected = test.expected.join(prettifyCSS.LINE_SEPARATOR) + + prettifyCSS.LINE_SEPARATOR; + equal(output, expected, test.name); + } +} diff --git a/devtools/shared/tests/unit/test_require.js b/devtools/shared/tests/unit/test_require.js new file mode 100644 index 000000000..005e56656 --- /dev/null +++ b/devtools/shared/tests/unit/test_require.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test require + +// Ensure that DevtoolsLoader.require doesn't spawn multiple +// loader/modules when early cached +function testBug1091706() { + let loader = new DevToolsLoader(); + let require = loader.require; + + let indent1 = require("devtools/shared/indentation"); + let indent2 = require("devtools/shared/indentation"); + + do_check_true(indent1 === indent2); +} + +function run_test() { + testBug1091706(); +} diff --git a/devtools/shared/tests/unit/test_require_lazy.js b/devtools/shared/tests/unit/test_require_lazy.js new file mode 100644 index 000000000..8ef5a7d23 --- /dev/null +++ b/devtools/shared/tests/unit/test_require_lazy.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test devtools.lazyRequireGetter + +function run_test() { + const name = "asyncUtils"; + const path = "devtools/shared/async-utils"; + const o = {}; + devtools.lazyRequireGetter(o, name, path); + const asyncUtils = require(path); + // XXX: do_check_eq only works on primitive types, so we have this + // do_check_true of an equality expression. + do_check_true(o.asyncUtils === asyncUtils); + + // A non-main loader should get a new object via |lazyRequireGetter|, just + // as it would via a direct |require|. + const o2 = {}; + let loader = new DevToolsLoader(); + + // We have to init the loader by loading any module before lazyRequireGetter is available + loader.require("devtools/shared/DevToolsUtils"); + + loader.lazyRequireGetter(o2, name, path); + do_check_true(o2.asyncUtils !== asyncUtils); + + // A module required via a non-main loader that then uses |lazyRequireGetter| + // should also get the same object from that non-main loader. + let exposeLoader = loader.require("xpcshell-test/exposeLoader"); + const o3 = exposeLoader.exerciseLazyRequire(name, path); + do_check_true(o3.asyncUtils === o2.asyncUtils); +} diff --git a/devtools/shared/tests/unit/test_require_raw.js b/devtools/shared/tests/unit/test_require_raw.js new file mode 100644 index 000000000..5bec82b55 --- /dev/null +++ b/devtools/shared/tests/unit/test_require_raw.js @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test require using "raw!". + +function run_test() { + let loader = new DevToolsLoader(); + let require = loader.require; + + let variableFileContents = require("raw!devtools/client/themes/variables.css"); + ok(variableFileContents.length > 0, "raw browserRequire worked"); + + let propertiesFileContents = require("raw!devtools/client/locales/shared.properties"); + ok(propertiesFileContents.length > 0, "unprefixed properties raw require worked"); + + let chromePropertiesFileContents = + require("raw!chrome://devtools/locale/shared.properties"); + ok(chromePropertiesFileContents.length > 0, "prefixed properties raw require worked"); +} diff --git a/devtools/shared/tests/unit/test_safeErrorString.js b/devtools/shared/tests/unit/test_safeErrorString.js new file mode 100644 index 000000000..9892f34d1 --- /dev/null +++ b/devtools/shared/tests/unit/test_safeErrorString.js @@ -0,0 +1,58 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test DevToolsUtils.safeErrorString + +function run_test() { + test_with_error(); + test_with_tricky_error(); + test_with_string(); + test_with_thrower(); + test_with_psychotic(); +} + +function test_with_error() { + let s = DevToolsUtils.safeErrorString(new Error("foo bar")); + // Got the message. + do_check_true(s.includes("foo bar")); + // Got the stack. + do_check_true(s.includes("test_with_error")); + do_check_true(s.includes("test_safeErrorString.js")); + // Got the lineNumber and columnNumber. + do_check_true(s.includes("Line")); + do_check_true(s.includes("column")); +} + +function test_with_tricky_error() { + let e = new Error("batman"); + e.stack = { toString: Object.create(null) }; + let s = DevToolsUtils.safeErrorString(e); + // Still got the message, despite a bad stack property. + do_check_true(s.includes("batman")); +} + +function test_with_string() { + let s = DevToolsUtils.safeErrorString("not really an error"); + // Still get the message. + do_check_true(s.includes("not really an error")); +} + +function test_with_thrower() { + let s = DevToolsUtils.safeErrorString({ + toString: () => { + throw new Error("Muahahaha"); + } + }); + // Still don't fail, get string back. + do_check_eq(typeof s, "string"); +} + +function test_with_psychotic() { + let s = DevToolsUtils.safeErrorString({ + toString: () => Object.create(null) + }); + // Still get a string out, and no exceptions thrown + do_check_eq(typeof s, "string"); + do_check_eq(s, "[object Object]"); +} diff --git a/devtools/shared/tests/unit/test_stack.js b/devtools/shared/tests/unit/test_stack.js new file mode 100644 index 000000000..ef747c83f --- /dev/null +++ b/devtools/shared/tests/unit/test_stack.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test stack.js. + +function run_test() { + let loader = new DevToolsLoader(); + let require = loader.require; + + const {StackFrameCache} = require("devtools/server/actors/utils/stack"); + + let cache = new StackFrameCache(); + cache.initFrames(); + let baseFrame = { + line: 23, + column: 77, + source: "nowhere", + functionDisplayName: "nobody", + parent: null, + asyncParent: null, + asyncCause: null + }; + cache.addFrame(baseFrame); + + let event = cache.makeEvent(); + do_check_eq(event[0], null); + do_check_eq(event[1].functionDisplayName, "nobody"); + do_check_eq(event.length, 2); + + cache.addFrame({ + line: 24, + column: 78, + source: "nowhere", + functionDisplayName: "still nobody", + parent: null, + asyncParent: baseFrame, + asyncCause: "async" + }); + + event = cache.makeEvent(); + do_check_eq(event[0].functionDisplayName, "still nobody"); + do_check_eq(event[0].parent, 0); + do_check_eq(event[0].asyncParent, 1); + do_check_eq(event.length, 1); +} diff --git a/devtools/shared/tests/unit/xpcshell.ini b/devtools/shared/tests/unit/xpcshell.ini new file mode 100644 index 000000000..d0323c813 --- /dev/null +++ b/devtools/shared/tests/unit/xpcshell.ini @@ -0,0 +1,40 @@ +[DEFAULT] +tags = devtools +head = head_devtools.js +tail = +firefox-appdir = browser +skip-if = toolkit == 'android' +support-files = + exposeLoader.js + +[test_assert.js] +[test_csslexer.js] +[test_css-properties-db.js] +# This test only enforces that the CSS database is up to date with nightly. The DB is +# only used when inspecting a target that doesn't support the getCSSDatabase actor. +# CSS properties are behind compile-time flags, and there is no automatic rebuild +# process for uplifts, so this test breaks on uplift. +run-if = nightly_build +[test_fetch-bom.js] +[test_fetch-chrome.js] +[test_fetch-file.js] +[test_fetch-http.js] +[test_fetch-resource.js] +[test_flatten.js] +[test_indentation.js] +[test_independent_loaders.js] +[test_invisible_loader.js] +[test_isSet.js] +[test_safeErrorString.js] +[test_defineLazyPrototypeGetter.js] +[test_async-utils.js] +[test_console_filtering.js] +[test_pluralForm-english.js] +[test_pluralForm-makeGetter.js] +[test_prettifyCSS.js] +[test_require_lazy.js] +[test_require_raw.js] +[test_require.js] +[test_stack.js] +[test_defer.js] +[test_executeSoon.js] |