diff options
Diffstat (limited to '')
37 files changed, 4026 insertions, 0 deletions
diff --git a/devtools/shared/tests/xpcshell/.eslintrc.js b/devtools/shared/tests/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..cc1ed286cc --- /dev/null +++ b/devtools/shared/tests/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/shared/tests/xpcshell/exposeLoader.js b/devtools/shared/tests/xpcshell/exposeLoader.js new file mode 100644 index 0000000000..7c8acdd759 --- /dev/null +++ b/devtools/shared/tests/xpcshell/exposeLoader.js @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +exports.exerciseLazyRequire = (name, path) => { + const o = {}; + loader.lazyRequireGetter(o, name, path); + return o; +}; diff --git a/devtools/shared/tests/xpcshell/head_devtools.js b/devtools/shared/tests/xpcshell/head_devtools.js new file mode 100644 index 0000000000..cee2218a2a --- /dev/null +++ b/devtools/shared/tests/xpcshell/head_devtools.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* exported DevToolsUtils, DevToolsLoader */ + +"use strict"; + +const { require, DevToolsLoader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); + +Services.prefs.setBoolPref("devtools.testing", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.testing"); +}); + +// 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; + +// XXX This listener is broken, see bug 1456634, for now turn off no-undef here, +// this needs turning back on! +/* eslint-disable no-undef */ +var listener = { + observe(message) { + let string; + try { + message.QueryInterface(Ci.nsIScriptError); + dump( + message.sourceName + + ":" + + message.lineNumber + + ": " + + scriptErrorFlagsToKind(message.flags) + + ": " + + message.errorMessage + + "\n" + ); + string = message.errorMessage; + } catch (ex) { + // Be a little paranoid with message, as the whole goal here is to lose + // no information. + try { + string = "" + message.message; + } catch (e) { + string = "<error converting error message to string>"; + } + } + + // Make sure we exit all nested event loops so that the test can finish. + while (DevToolsServer.xpcInspector.eventLoopNestLevel > 0) { + DevToolsServer.xpcInspector.exitNestedEventLoop(); + } + + if (!ALLOW_CONSOLE_ERRORS) { + do_throw("head_devtools.js got console message: " + string + "\n"); + } + }, +}; +/* eslint-enable no-undef */ + +Services.console.registerListener(listener); diff --git a/devtools/shared/tests/xpcshell/test_assert.js b/devtools/shared/tests/xpcshell/test_assert.js new file mode 100644 index 0000000000..45ae9eb1a2 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_assert.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test DevToolsUtils.assert + +ALLOW_CONSOLE_ERRORS = true; + +function run_test() { + 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/xpcshell/test_console_filtering.js b/devtools/shared/tests/xpcshell/test_console_filtering.js new file mode 100644 index 0000000000..681b93e523 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_console_filtering.js @@ -0,0 +1,156 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" +); +const { + ConsoleAPIListener, +} = require("resource://devtools/server/actors/webconsole/listeners/console-api.js"); + +var seenMessages = 0; +var seenTypes = 0; + +var onConsoleAPICall = function (message) { + if (message.consoleID && message.consoleID == "addon/foo") { + Assert.equal(message.level, "warn"); + Assert.equal(message.arguments[0], "Warning from foo"); + seenTypes |= 1; + } else if (message.addonId == "bar") { + Assert.equal(message.level, "error"); + Assert.equal(message.arguments[0], "Error from bar"); + seenTypes |= 2; + } else { + Assert.equal(message.level, "log"); + Assert.equal(message.arguments[0], "Hello from default console"); + seenTypes |= 4; + } + seenMessages++; +}; + +let policy; +registerCleanupFunction(() => { + policy.active = false; +}); + +function createFakeAddonWindow({ addonId } = {}) { + const uuidGen = Services.uuid; + const uuid = uuidGen.generateUUID().number.slice(1, -1); + + if (policy) { + policy.active = false; + } + /* globals MatchPatternSet, WebExtensionPolicy */ + policy = new WebExtensionPolicy({ + id: addonId, + mozExtensionHostname: uuid, + baseURL: "file:///", + allowedOrigins: new MatchPatternSet([]), + localizeCallback() {}, + }); + policy.active = true; + + const baseURI = Services.io.newURI(`moz-extension://${uuid}/`); + const principal = Services.scriptSecurityManager.createContentPrincipal( + baseURI, + {} + ); + const chromeWebNav = Services.appShell.createWindowlessBrowser(true); + const { docShell } = chromeWebNav; + docShell.createAboutBlankDocumentViewer(principal, principal); + const addonWindow = docShell.docViewer.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.sys.mjs messages tagged by the Addon SDK + // are still filtered correctly. + const console1 = new ConsoleAPI({ + consoleID: "addon/foo", + }); + + // console2 - WebExtension page's console messages tagged + // by 'originAttributes.addonId' are filtered correctly. + const { addonWindow, chromeWebNav } = createFakeAddonWindow({ + addonId: "bar", + }); + const 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, onConsoleAPICall); + listener.init(); + let messages = listener.getCachedMessages(); + + seenTypes = 0; + seenMessages = 0; + messages.forEach(onConsoleAPICall); + Assert.equal(seenMessages, 3); + Assert.equal(seenTypes, 7); + + seenTypes = 0; + seenMessages = 0; + console.log("Hello from default console"); + console1.warn("Warning from foo"); + console2.error("Error from bar"); + Assert.equal(seenMessages, 3); + Assert.equal(seenTypes, 7); + + listener.destroy(); + + listener = new ConsoleAPIListener(null, onConsoleAPICall, { addonId: "foo" }); + listener.init(); + messages = listener.getCachedMessages(); + + seenTypes = 0; + seenMessages = 0; + messages.forEach(onConsoleAPICall); + Assert.equal(seenMessages, 2); + Assert.equal(seenTypes, 1); + + seenTypes = 0; + seenMessages = 0; + console.log("Hello from default console"); + console1.warn("Warning from foo"); + console2.error("Error from bar"); + Assert.equal(seenMessages, 1); + Assert.equal(seenTypes, 1); + + listener.destroy(); + + listener = new ConsoleAPIListener(null, onConsoleAPICall, { addonId: "bar" }); + listener.init(); + messages = listener.getCachedMessages(); + + seenTypes = 0; + seenMessages = 0; + messages.forEach(onConsoleAPICall); + Assert.equal(seenMessages, 3); + Assert.equal(seenTypes, 2); + + seenTypes = 0; + seenMessages = 0; + console.log("Hello from default console"); + console1.warn("Warning from foo"); + console2.error("Error from bar"); + + Assert.equal(seenMessages, 1); + Assert.equal(seenTypes, 2); + + listener.destroy(); + + // Close the addon window's chromeWebNav. + chromeWebNav.close(); +} diff --git a/devtools/shared/tests/xpcshell/test_csslexer.js b/devtools/shared/tests/xpcshell/test_csslexer.js new file mode 100644 index 0000000000..417fb6b1df --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_csslexer.js @@ -0,0 +1,203 @@ +/* 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"; + +const jsLexer = require("resource://devtools/shared/css/lexer.js"); + +function test_lexer(cssText, tokenTypes) { + const lexer = jsLexer.getCSSLexer(cssText); + let reconstructed = ""; + let lastTokenEnd = 0; + let i = 0; + while (true) { + const token = lexer.nextToken(); + if (!token) { + break; + } + let combined = token.tokenType; + if (token.text) { + combined += ":" + token.text; + } + equal(combined, tokenTypes[i]); + Assert.greater(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"]], + ["url(http://example.com @", ["bad_url:http://example.com"]], + ["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) { + const lexer = jsLexer.getCSSLexer(cssText); + let i = 0; + while (true) { + const token = lexer.nextToken(); + const startLine = lexer.lineNumber; + const 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 +) { + const lexer = jsLexer.getCSSLexer(cssText); + while (lexer.nextToken()) { + // Nothing. + } + + info("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/xpcshell/test_debugger_client.js b/devtools/shared/tests/xpcshell/test_debugger_client.js new file mode 100644 index 0000000000..d908ddfb27 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_debugger_client.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// DevToolsClient tests + +const { + DevToolsServer, +} = require("resource://devtools/server/devtools-server.js"); +const { + DevToolsClient, +} = require("resource://devtools/client/devtools-client.js"); + +add_task(async function () { + await testCloseLoops(); + await fakeTransportShutdown(); +}); + +function createClient() { + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + const client = new DevToolsClient(DevToolsServer.connectPipe()); + return client; +} + +// Ensure that closing the client while it is closing doesn't loop +async function testCloseLoops() { + const client = createClient(); + await client.connect(); + + await new Promise(resolve => { + let called = false; + client.on("closed", async () => { + dump(">> CLOSED\n"); + if (called) { + ok( + false, + "Calling client.close from closed event listener introduce loops" + ); + return; + } + called = true; + await client.close(); + resolve(); + }); + client.close(); + }); +} + +// Check that, if we fake a transport shutdown (like if a device is unplugged) +// the client is automatically closed, and we can still call client.close. +async function fakeTransportShutdown() { + const client = createClient(); + await client.connect(); + + await new Promise(resolve => { + const onClosed = async function () { + client.off("closed", onClosed); + ok(true, "Client emitted 'closed' event"); + resolve(); + }; + client.on("closed", onClosed); + client.transport.close(); + }); + + await client.close(); + ok(true, "client.close() successfully resolves"); +} diff --git a/devtools/shared/tests/xpcshell/test_defineLazyPrototypeGetter.js b/devtools/shared/tests/xpcshell/test_defineLazyPrototypeGetter.js new file mode 100644 index 0000000000..2f53b377de --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_defineLazyPrototypeGetter.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// 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. + const descriptor = Object.getOwnPropertyDescriptor(Class.prototype, "foo"); + Assert.equal(typeof descriptor.get, "function"); + Assert.equal(descriptor.set, undefined); + Assert.equal(descriptor.enumerable, false); + Assert.equal(descriptor.configurable, true); +} + +function test_instance_attributes() { + // Instances should not have an own property until the lazy getter has been + // activated. + const instance = new Class(); + Assert.ok(!instance.hasOwnProperty("foo")); + instance.foo; + Assert.ok(instance.hasOwnProperty("foo")); + + // Check that the instance has an own property with the expecred value and + // attributes after the lazy getter is activated. + const descriptor = Object.getOwnPropertyDescriptor(instance, "foo"); + Assert.ok(descriptor.value instanceof Array); + Assert.equal(descriptor.writable, true); + Assert.equal(descriptor.enumerable, false); + Assert.equal(descriptor.configurable, true); +} + +function test_multiple_instances() { + const instance1 = new Class(); + const instance2 = new Class(); + const foo1 = instance1.foo; + const foo2 = instance2.foo; + // Check that the lazy getter returns the expected type of value. + Assert.ok(foo1 instanceof Array); + Assert.ok(foo2 instanceof Array); + // Make sure the lazy getter runs once and only once per instance. + Assert.equal(instance1.foo, foo1); + Assert.equal(instance2.foo, foo2); + // Make sure each instance gets its own unique value. + Assert.notEqual(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. + const instance = new Foo(); + Assert.equal(instance.foo, instance); +} diff --git a/devtools/shared/tests/xpcshell/test_eventemitter_abort_controller.js b/devtools/shared/tests/xpcshell/test_eventemitter_abort_controller.js new file mode 100644 index 0000000000..9a35ce1f98 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_eventemitter_abort_controller.js @@ -0,0 +1,181 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const EventEmitter = require("resource://devtools/shared/event-emitter.js"); + +add_task(function testAbortSingleListener() { + // Test a simple case with AbortController + info("Create an EventEmitter"); + const emitter = new EventEmitter(); + const abortController = new AbortController(); + const { signal } = abortController; + + info("Setup an event listener on test-event, controlled by an AbortSignal"); + let eventsReceived = 0; + emitter.on("test-event", () => eventsReceived++, { signal }); + + info("Emit test-event"); + emitter.emit("test-event"); + equal(eventsReceived, 1, "We received one event, as expected"); + + info("Abort the AbortController…"); + abortController.abort(); + info("… and emit test-event again"); + emitter.emit("test-event"); + equal(eventsReceived, 1, "We didn't receive new event after aborting"); +}); + +add_task(function testAbortSingleListenerOnce() { + // Test a simple case with AbortController and once + info("Create an EventEmitter"); + const emitter = new EventEmitter(); + const abortController = new AbortController(); + const { signal } = abortController; + + info("Setup an event listener on test-event, controlled by an AbortSignal"); + let eventReceived = false; + emitter.once( + "test-event", + () => { + eventReceived = true; + }, + { signal } + ); + + info("Abort the AbortController…"); + abortController.abort(); + info("… and emit test-event"); + emitter.emit("test-event"); + equal(eventReceived, false, "We didn't receive the event after aborting"); +}); + +add_task(function testAbortMultipleListener() { + // Test aborting multiple event listeners with one call to abort + info("Create an EventEmitter"); + const emitter = new EventEmitter(); + const abortController = new AbortController(); + const { signal } = abortController; + + info("Setup 3 event listeners controlled by an AbortSignal"); + let eventsReceived = 0; + emitter.on("test-event", () => eventsReceived++, { signal }); + emitter.on("test-event", () => eventsReceived++, { signal }); + emitter.on("other-test-event", () => eventsReceived++, { signal }); + + info("Emit test-event and other-test-event"); + emitter.emit("test-event"); + emitter.emit("other-test-event"); + equal(eventsReceived, 3, "We received 3 events, as expected"); + + info("Abort the AbortController…"); + abortController.abort(); + info("… and emit events again"); + emitter.emit("test-event"); + emitter.emit("other-test-event"); + equal(eventsReceived, 3, "We didn't receive new event after aborting"); +}); + +add_task(function testAbortMultipleEmitter() { + // Test aborting multiple event listeners on different emitters with one call to abort + info("Create 2 EventEmitter"); + const emitter1 = new EventEmitter(); + const emitter2 = new EventEmitter(); + const abortController = new AbortController(); + const { signal } = abortController; + + info("Setup 2 event listeners on test-event, controlled by an AbortSignal"); + let eventsReceived = 0; + emitter1.on("test-event", () => eventsReceived++, { signal }); + emitter2.on("other-test-event", () => eventsReceived++, { signal }); + + info("Emit test-event and other-test-event"); + emitter1.emit("test-event"); + emitter2.emit("other-test-event"); + equal(eventsReceived, 2, "We received 2 events, as expected"); + + info("Abort the AbortController…"); + abortController.abort(); + info("… and emit events again"); + emitter1.emit("test-event"); + emitter2.emit("other-test-event"); + equal(eventsReceived, 2, "We didn't receive new event after aborting"); +}); + +add_task(function testAbortBeforeEmitting() { + // Check that aborting before emitting does unregister the event listener + info("Create an EventEmitter"); + const emitter = new EventEmitter(); + const abortController = new AbortController(); + const { signal } = abortController; + + info("Setup an event listener on test-event, controlled by an AbortSignal"); + let eventsReceived = 0; + emitter.on("test-event", () => eventsReceived++, { signal }); + + info("Abort the AbortController…"); + abortController.abort(); + + info("… and emit test-event"); + emitter.emit("test-event"); + equal(eventsReceived, 0, "We didn't receive any event"); +}); + +add_task(function testAbortBeforeSettingListener() { + // Check that aborting before creating the event listener won't register it + info("Create an EventEmitter"); + const emitter = new EventEmitter(); + + info("Create an AbortController and abort it immediately"); + const abortController = new AbortController(); + const { signal } = abortController; + abortController.abort(); + + info( + "Setup an event listener on test-event, controlled by the aborted AbortSignal" + ); + let eventsReceived = 0; + const off = emitter.on("test-event", () => eventsReceived++, { signal }); + + info("Emit test-event"); + emitter.emit("test-event"); + equal(eventsReceived, 0, "We didn't receive any event"); + + equal(typeof off, "function", "emitter.on still returned a function"); + // check that calling off does not throw + off(); +}); + +add_task(function testAbortAfterEventListenerIsRemoved() { + // Check that aborting after there's no more event listener does not throw + info("Create an EventEmitter"); + const emitter = new EventEmitter(); + + const abortController = new AbortController(); + const { signal } = abortController; + + info( + "Setup an event listener on test-event, controlled by the aborted AbortSignal" + ); + let eventsReceived = 0; + const off = emitter.on("test-event", () => eventsReceived++, { signal }); + + info("Emit test-event"); + emitter.emit("test-event"); + equal(eventsReceived, 1, "We received the expected event"); + + info("Remove the event listener with the function returned by `on`"); + off(); + + info("Emit test-event a second time"); + emitter.emit("test-event"); + equal( + eventsReceived, + 1, + "We didn't receive new event after removing the event listener" + ); + + info("Abort to check it doesn't throw"); + abortController.abort(); +}); diff --git a/devtools/shared/tests/xpcshell/test_eventemitter_basic.js b/devtools/shared/tests/xpcshell/test_eventemitter_basic.js new file mode 100644 index 0000000000..caf2186bff --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_eventemitter_basic.js @@ -0,0 +1,345 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + ConsoleAPIListener, +} = require("resource://devtools/server/actors/webconsole/listeners/console-api.js"); +const EventEmitter = require("resource://devtools/shared/event-emitter.js"); +const hasMethod = (target, method) => + method in target && typeof target[method] === "function"; + +/** + * Each method of this object is a test; tests can be synchronous or asynchronous: + * + * 1. Plain functions are synchronous tests. + * 2. methods with `async` keyword are asynchronous tests. + * 3. methods with `done` as argument are asynchronous tests (`done` needs to be called to + * finish the test). + */ +const TESTS = { + testEventEmitterCreation() { + const emitter = getEventEmitter(); + const isAnEmitter = emitter instanceof EventEmitter; + + ok(emitter, "We have an event emitter"); + ok( + hasMethod(emitter, "on") && + hasMethod(emitter, "off") && + hasMethod(emitter, "once") && + hasMethod(emitter, "count") && + !hasMethod(emitter, "decorate"), + `Event Emitter ${ + isAnEmitter ? "instance" : "mixin" + } has the expected methods.` + ); + }, + + testEmittingEvents(done) { + const emitter = getEventEmitter(); + + let beenHere1 = false; + let beenHere2 = false; + + function next(str1, str2) { + equal(str1, "abc", "Argument 1 is correct"); + equal(str2, "def", "Argument 2 is correct"); + + ok(!beenHere1, "first time in next callback"); + beenHere1 = true; + + emitter.off("next", next); + + emitter.emit("next"); + + emitter.once("onlyonce", onlyOnce); + + emitter.emit("onlyonce"); + emitter.emit("onlyonce"); + } + + function onlyOnce() { + ok(!beenHere2, '"once" listener has been called once'); + beenHere2 = true; + emitter.emit("onlyonce"); + + done(); + } + + emitter.on("next", next); + emitter.emit("next", "abc", "def"); + }, + + testThrowingExceptionInListener(done) { + const emitter = getEventEmitter(); + const listener = new ConsoleAPIListener(null, message => { + equal(message.level, "error"); + const [arg] = message.arguments; + equal(arg.message, "foo"); + equal(arg.stack, "bar"); + listener.destroy(); + done(); + }); + + listener.init(); + + function throwListener() { + emitter.off("throw-exception"); + const err = new Error("foo"); + err.stack = "bar"; + throw err; + } + + emitter.on("throw-exception", throwListener); + emitter.emit("throw-exception"); + }, + + testKillItWhileEmitting(done) { + const emitter = getEventEmitter(); + + const c1 = () => ok(true, "c1 called"); + const c2 = () => { + ok(true, "c2 called"); + emitter.off("tick", c3); + }; + const c3 = () => ok(false, "c3 should not be called"); + const c4 = () => { + ok(true, "c4 called"); + done(); + }; + + emitter.on("tick", c1); + emitter.on("tick", c2); + emitter.on("tick", c3); + emitter.on("tick", c4); + + emitter.emit("tick"); + }, + + testOffAfterOnce() { + const emitter = getEventEmitter(); + + let enteredC1 = false; + const c1 = () => (enteredC1 = true); + + emitter.once("oao", c1); + emitter.off("oao", c1); + + emitter.emit("oao"); + + ok(!enteredC1, "c1 should not be called"); + }, + + testPromise() { + const emitter = getEventEmitter(); + const p = emitter.once("thing"); + + // Check that the promise is only resolved once event though we + // emit("thing") more than once + let firstCallbackCalled = false; + const check1 = p.then(arg => { + equal(firstCallbackCalled, false, "first callback called only once"); + firstCallbackCalled = true; + equal(arg, "happened", "correct arg in promise"); + return "rval from c1"; + }); + + emitter.emit("thing", "happened", "ignored"); + + // Check that the promise is resolved asynchronously + let secondCallbackCalled = false; + const check2 = p.then(arg => { + ok(true, "second callback called"); + equal(arg, "happened", "correct arg in promise"); + secondCallbackCalled = true; + equal(arg, "happened", "correct arg in promise (a second time)"); + return "rval from c2"; + }); + + // Shouldn't call any of the above listeners + emitter.emit("thing", "trashinate"); + + // Check that we can still separate events with different names + // and that it works with no parameters + const pfoo = emitter.once("foo"); + const pbar = emitter.once("bar"); + + const check3 = pfoo.then(arg => { + Assert.strictEqual(arg, undefined, "no arg for foo event"); + return "rval from c3"; + }); + + pbar.then(() => { + ok(false, "pbar should not be called"); + }); + + emitter.emit("foo"); + + equal(secondCallbackCalled, false, "second callback not called yet"); + + return Promise.all([check1, check2, check3]).then(args => { + equal(args[0], "rval from c1", "callback 1 done good"); + equal(args[1], "rval from c2", "callback 2 done good"); + equal(args[2], "rval from c3", "callback 3 done good"); + }); + }, + + testClearEvents() { + const emitter = getEventEmitter(); + + const received = []; + const listener = (...args) => received.push(args); + + emitter.on("a", listener); + emitter.on("b", listener); + emitter.on("c", listener); + + emitter.emit("a", 1); + emitter.emit("b", 1); + emitter.emit("c", 1); + + equal(received.length, 3, "the listener was triggered three times"); + + emitter.clearEvents(); + emitter.emit("a", 1); + emitter.emit("b", 1); + emitter.emit("c", 1); + equal(received.length, 3, "the listener was not called after clearEvents"); + }, + + testOnReturn() { + const emitter = getEventEmitter(); + + let called = false; + const removeOnTest = emitter.on("test", () => { + called = true; + }); + + equal(typeof removeOnTest, "function", "`on` returns a function"); + removeOnTest(); + + emitter.emit("test"); + equal(called, false, "event listener wasn't called"); + }, + + async testEmitAsync() { + const emitter = getEventEmitter(); + + let resolve1, resolve2; + emitter.once("test", async () => { + return new Promise(r => { + resolve1 = r; + }); + }); + + // Adding a listener which doesn't return a promise should trigger a console warning. + emitter.once("test", () => {}); + + emitter.once("test", async () => { + return new Promise(r => { + resolve2 = r; + }); + }); + + info("Emit an event and wait for all listener resolutions"); + const onConsoleWarning = onConsoleWarningLogged( + "Listener for event 'test' did not return a promise." + ); + const onEmitted = emitter.emitAsync("test"); + let resolved = false; + onEmitted.then(() => { + info("emitAsync just resolved"); + resolved = true; + }); + + info("Waiting for warning message about the second listener"); + await onConsoleWarning; + + // Spin the event loop, to ensure that emitAsync did not resolved too early + await new Promise(r => Services.tm.dispatchToMainThread(r)); + + ok(resolve1, "event listener has been called"); + ok(!resolved, "but emitAsync hasn't resolved yet"); + + info("Resolve the first listener function"); + resolve1(); + ok(!resolved, "emitAsync isn't resolved until all listener resolve"); + + info("Resolve the second listener function"); + resolve2(); + + // emitAsync is only resolved in the next event loop + await new Promise(r => Services.tm.dispatchToMainThread(r)); + ok(resolved, "once we resolve all the listeners, emitAsync is resolved"); + }, + + testCount() { + const emitter = getEventEmitter(); + + equal(emitter.count("foo"), 0, "no listeners for 'foo' events"); + emitter.on("foo", () => {}); + equal(emitter.count("foo"), 1, "listener registered"); + emitter.on("foo", () => {}); + equal(emitter.count("foo"), 2, "another listener registered"); + emitter.off("foo"); + equal(emitter.count("foo"), 0, "listeners unregistered"); + }, +}; + +// Wait for the next call to console.warn which includes +// the text passed as argument +function onConsoleWarningLogged(warningMessage) { + return new Promise(resolve => { + const ConsoleAPIStorage = Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(Ci.nsIConsoleAPIStorage); + + const observer = subject => { + // This is the first argument passed to console.warn() + const message = subject.wrappedJSObject.arguments[0]; + if (message.includes(warningMessage)) { + ConsoleAPIStorage.removeLogEventListener(observer); + resolve(); + } + }; + + ConsoleAPIStorage.addLogEventListener( + observer, + Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal) + ); + }); +} + +/** + * Create a runnable tests based on the tests descriptor given. + * + * @param {Object} tests + * The tests descriptor object, contains the tests to run. + */ +const runnable = tests => + async function () { + for (const name of Object.keys(tests)) { + info(name); + if (tests[name].length === 1) { + await new Promise(resolve => tests[name](resolve)); + } else { + await tests[name](); + } + } + }; + +// We want to run the same tests for both an instance of `EventEmitter` and an object +// decorate with EventEmitter; therefore we create two strategies (`createNewEmitter` and +// `decorateObject`) and a factory (`getEventEmitter`), where the factory is the actual +// function used in the tests. + +const createNewEmitter = () => new EventEmitter(); +const decorateObject = () => EventEmitter.decorate({}); + +// First iteration of the tests with a new instance of `EventEmitter`. +let getEventEmitter = createNewEmitter; +add_task(runnable(TESTS)); +// Second iteration of the tests with an object decorate using `EventEmitter` +add_task(() => (getEventEmitter = decorateObject)); +add_task(runnable(TESTS)); diff --git a/devtools/shared/tests/xpcshell/test_eventemitter_destroy.js b/devtools/shared/tests/xpcshell/test_eventemitter_destroy.js new file mode 100644 index 0000000000..715dd1c466 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_eventemitter_destroy.js @@ -0,0 +1,32 @@ +/* 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"; + +add_task(function () { + const { DevToolsLoader, require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + + // Force-load the module once in the global loader to avoid Bug 1622718. + require("resource://devtools/shared/event-emitter.js"); + + const emitterRef = (function () { + const loader = new DevToolsLoader(); + + const ref = Cu.getWeakReference( + loader.require("resource://devtools/shared/event-emitter.js") + ); + + loader.destroy(); + return ref; + })(); + + Cu.forceGC(); + Cu.forceCC(); + Cu.forceGC(); + Cu.forceCC(); + + Assert.ok(!emitterRef.get(), "weakref has been cleared by gc"); +}); diff --git a/devtools/shared/tests/xpcshell/test_eventemitter_static.js b/devtools/shared/tests/xpcshell/test_eventemitter_static.js new file mode 100644 index 0000000000..9b17a7612f --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_eventemitter_static.js @@ -0,0 +1,378 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + ConsoleAPIListener, +} = require("resource://devtools/server/actors/webconsole/listeners/console-api.js"); +const { + on, + once, + off, + emit, + count, + handler, +} = require("resource://devtools/shared/event-emitter.js"); + +const pass = message => ok(true, message); +const fail = message => ok(false, message); + +/** + * Each method of this object is a test; tests can be synchronous or asynchronous: + * + * 1. Plain method are synchronous tests. + * 2. methods with `async` keyword are asynchronous tests. + * 3. methods with `done` as argument are asynchronous tests (`done` needs to be called to + * complete the test). + */ +const TESTS = { + testAddListener() { + const events = [{ name: "event#1" }, "event#2"]; + const target = { name: "target" }; + + on(target, "message", function (message) { + equal(this, target, "this is a target object"); + equal(message, events.shift(), "message is emitted event"); + }); + + emit(target, "message", events[0]); + emit(target, "message", events[0]); + }, + + testListenerIsUniquePerType() { + const actual = []; + const target = {}; + listener = () => actual.push(1); + + on(target, "message", listener); + on(target, "message", listener); + on(target, "message", listener); + on(target, "foo", listener); + on(target, "foo", listener); + + emit(target, "message"); + deepEqual([1], actual, "only one message listener added"); + + emit(target, "foo"); + deepEqual([1, 1], actual, "same listener added for other event"); + }, + + testEventTypeMatters() { + const target = { name: "target" }; + on(target, "message", () => fail("no event is expected")); + on(target, "done", () => pass("event is emitted")); + + emit(target, "foo"); + emit(target, "done"); + }, + + testAllArgumentsArePassed() { + const foo = { name: "foo" }, + bar = "bar"; + const target = { name: "target" }; + + on(target, "message", (a, b) => { + equal(a, foo, "first argument passed"); + equal(b, bar, "second argument passed"); + }); + + emit(target, "message", foo, bar); + }, + + testNoSideEffectsInEmit() { + const target = { name: "target" }; + + on(target, "message", () => { + pass("first listener is called"); + + on(target, "message", () => fail("second listener is called")); + }); + emit(target, "message"); + }, + + testCanRemoveNextListener() { + const target = { name: "target" }; + + on(target, "data", () => { + pass("first listener called"); + off(target, "data", fail); + }); + on(target, "data", fail); + + emit(target, "data", "Listener should be removed"); + }, + + testOrderOfPropagation() { + const actual = []; + const target = { name: "target" }; + + on(target, "message", () => actual.push(1)); + on(target, "message", () => actual.push(2)); + on(target, "message", () => actual.push(3)); + emit(target, "message"); + + deepEqual([1, 2, 3], actual, "called in order they were added"); + }, + + testRemoveListener() { + const target = { name: "target" }; + const actual = []; + + on(target, "message", function listener() { + actual.push(1); + on(target, "message", () => { + off(target, "message", listener); + actual.push(2); + }); + }); + + emit(target, "message"); + deepEqual([1], actual, "first listener called"); + + emit(target, "message"); + deepEqual([1, 1, 2], actual, "second listener called"); + + emit(target, "message"); + deepEqual([1, 1, 2, 2, 2], actual, "first listener removed"); + }, + + testRemoveAllListenersForType() { + const actual = []; + const target = { name: "target" }; + + on(target, "message", () => actual.push(1)); + on(target, "message", () => actual.push(2)); + on(target, "message", () => actual.push(3)); + on(target, "bar", () => actual.push("b")); + off(target, "message"); + + emit(target, "message"); + emit(target, "bar"); + + deepEqual(["b"], actual, "all message listeners were removed"); + }, + + testRemoveAllListeners() { + const actual = []; + const target = { name: "target" }; + + on(target, "message", () => actual.push(1)); + on(target, "message", () => actual.push(2)); + on(target, "message", () => actual.push(3)); + on(target, "bar", () => actual.push("b")); + + off(target); + + emit(target, "message"); + emit(target, "bar"); + + deepEqual([], actual, "all listeners events were removed"); + }, + + testFalsyArgumentsAreFine() { + let type, listener; + const target = { name: "target" }, + actual = []; + on(target, "bar", () => actual.push(0)); + + off(target, "bar", listener); + emit(target, "bar"); + deepEqual([0], actual, "3rd bad arg will keep listener"); + + off(target, type); + emit(target, "bar"); + deepEqual([0, 0], actual, "2nd bad arg will keep listener"); + + off(target, type, listener); + emit(target, "bar"); + deepEqual([0, 0, 0], actual, "2nd & 3rd bad args will keep listener"); + }, + + testUnhandledExceptions(done) { + const listener = new ConsoleAPIListener(null, message => { + equal(message.level, "error", "Got the first exception"); + equal( + message.arguments[0].message, + "Boom!", + "unhandled exception is logged" + ); + + listener.destroy(); + done(); + }); + + listener.init(); + + const target = {}; + + on(target, "message", () => { + throw Error("Boom!"); + }); + + emit(target, "message"); + }, + + testCount() { + const target = { name: "target" }; + + equal(count(target, "foo"), 0, "no listeners for 'foo' events"); + on(target, "foo", () => {}); + equal(count(target, "foo"), 1, "listener registered"); + on(target, "foo", () => {}); + equal(count(target, "foo"), 2, "another listener registered"); + off(target); + equal(count(target, "foo"), 0, "listeners unregistered"); + }, + + async testOnce() { + const target = { name: "target" }; + const called = false; + + const pFoo = once(target, "foo", function (value) { + ok(!called, "listener called only once"); + equal(value, "bar", "correct argument was passed"); + equal(this, target, "the contextual object is correct"); + }); + const pDone = once(target, "done"); + + emit(target, "foo", "bar"); + emit(target, "foo", "baz"); + emit(target, "done", ""); + + await Promise.all([pFoo, pDone]); + }, + + testRemovingOnce(done) { + const target = { name: "target" }; + + once(target, "foo", fail); + once(target, "done", done); + + off(target, "foo", fail); + + emit(target, "foo", "listener was called"); + emit(target, "done", ""); + }, + + testAddListenerWithHandlerMethod() { + const target = { name: "target" }; + const actual = []; + const listener = function (...args) { + equal( + this, + target, + "the contextual object is correct for function listener" + ); + deepEqual(args, [10, 20, 30], "arguments are properly passed"); + }; + + const object = { + name: "target", + [handler](type, ...rest) { + actual.push(type); + equal( + this, + object, + "the contextual object is correct for object listener" + ); + deepEqual(rest, [10, 20, 30], "arguments are properly passed"); + }, + }; + + on(target, "foo", listener); + on(target, "bar", object); + on(target, "baz", object); + + emit(target, "foo", 10, 20, 30); + emit(target, "bar", 10, 20, 30); + emit(target, "baz", 10, 20, 30); + + deepEqual( + actual, + ["bar", "baz"], + "object's listener called in the expected order" + ); + }, + + testRemoveListenerWithHandlerMethod() { + const target = {}; + const actual = []; + + const object = { + [handler](type) { + actual.push(1); + on(target, "message", () => { + off(target, "message", object); + actual.push(2); + }); + }, + }; + + on(target, "message", object); + + emit(target, "message"); + deepEqual([1], actual, "first listener called"); + + emit(target, "message"); + deepEqual([1, 1, 2], actual, "second listener called"); + + emit(target, "message"); + deepEqual([1, 1, 2, 2, 2], actual, "first listener removed"); + }, + + async testOnceListenerWithHandlerMethod() { + const target = { name: "target" }; + const called = false; + + const object = { + [handler](type, value) { + ok(!called, "listener called only once"); + equal(type, "foo", "event type is properly passed"); + equal(value, "bar", "correct argument was passed"); + equal( + this, + object, + "the contextual object is correct for object listener" + ); + }, + }; + + const pFoo = once(target, "foo", object); + + const pDone = once(target, "done"); + + emit(target, "foo", "bar"); + emit(target, "foo", "baz"); + emit(target, "done", ""); + + await Promise.all([pFoo, pDone]); + }, + + testCallingOffWithMoreThan3Args() { + const target = { name: "target" }; + on(target, "data", fail); + off(target, "data", fail, undefined); + emit(target, "data", "Listener should be removed"); + }, +}; + +/** + * Create a runnable tests based on the tests descriptor given. + * + * @param {Object} tests + * The tests descriptor object, contains the tests to run. + */ +const runnable = tests => + async function () { + for (const name of Object.keys(tests)) { + info(name); + if (tests[name].length === 1) { + await new Promise(resolve => tests[name](resolve)); + } else { + await tests[name](); + } + } + }; + +add_task(runnable(TESTS)); diff --git a/devtools/shared/tests/xpcshell/test_executeSoon.js b/devtools/shared/tests/xpcshell/test_executeSoon.js new file mode 100644 index 0000000000..acb60360a1 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_executeSoon.js @@ -0,0 +1,35 @@ +/* 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("resource://devtools/shared/DevToolsUtils.js"); + +add_task(async function () { + await waitForTick(); + + let stack = Components.stack; + while (stack) { + info(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() { + return new Promise(resolve => { + executeSoon(resolve); + }); +} diff --git a/devtools/shared/tests/xpcshell/test_fetch-bom.js b/devtools/shared/tests/xpcshell/test_fetch-bom.js new file mode 100644 index 0000000000..566133fda6 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_fetch-bom.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Tests for DevToolsUtils.fetch BOM detection. + +const { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); +const BinaryOutputStream = Components.Constructor( + "@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"); + + const 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_get_profile(); + +registerCleanupFunction(() => { + return new Promise(resolve => server.stop(resolve)); +}); + +add_task(async function () { + await test_one(serverURL + "/u8", "UTF-8"); + await test_one(serverURL + "/u16be", "UTF-16BE"); + await test_one(serverURL + "/u16le", "UTF-16LE"); +}); + +async 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. + await DevToolsUtils.fetch(url, { charset: "ISO-8859-1" }).then( + ({ content }) => { + Assert.equal(content, "hı", "The content looks correct for " + encoding); + } + ); +} diff --git a/devtools/shared/tests/xpcshell/test_fetch-chrome.js b/devtools/shared/tests/xpcshell/test_fetch-chrome.js new file mode 100644 index 0000000000..38021d49c9 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_fetch-chrome.js @@ -0,0 +1,36 @@ +/* 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(async function test_missing() { + await DevToolsUtils.fetch(URL_NOT_FOUND).then( + result => { + info(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(async function test_normal() { + await DevToolsUtils.fetch(URL_FOUND).then(result => { + notDeepEqual( + result.content, + "", + "chrome:// URI seems to be read correctly." + ); + }); +}); diff --git a/devtools/shared/tests/xpcshell/test_fetch-file.js b/devtools/shared/tests/xpcshell/test_fetch-file.js new file mode 100644 index 0000000000..47d3f52681 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_fetch-file.js @@ -0,0 +1,113 @@ +/* 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 } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); + +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.sys.mjs -> + * file://l10n.js' should load 'file://l10n.js'. + */ +add_task(async function test_arrow_urls() { + const { path } = createTemporaryFile(".js"); + const url = "resource://gre/modules/XPIProvider.sys.mjs -> file://" + path; + + await IOUtils.writeUTF8(path, TEST_CONTENT); + const { content } = await DevToolsUtils.fetch(url); + + deepEqual(content, TEST_CONTENT, "The file contents were correctly read."); +}); + +/** + * Tests that empty files are read correctly. + */ +add_task(async function test_empty() { + const { path } = createTemporaryFile(); + const { content } = await DevToolsUtils.fetch("file://" + path); + deepEqual(content, "", "The empty file was read correctly."); +}); + +/** + * Tests that UTF-8 encoded files are correctly read. + */ +add_task(async function test_encoding_utf8() { + const { path } = createTemporaryFile(); + await IOUtils.write(path, UTF8_TEST_BUFFER); + + const { content } = await 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(async function test_encoding_iso_8859_1() { + const { path } = createTemporaryFile(); + await IOUtils.write(path, ISO_8859_1_BUFFER); + + const { content } = await 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(async function test_missing() { + await DevToolsUtils.fetch("file:///file/not/found.right").then( + result => { + info(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(async function test_schemeless_files() { + const { path } = createTemporaryFile(); + + await IOUtils.writeUTF8(path, TEST_CONTENT); + + const { content } = await 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) { + const name = "test_fetch-file-" + Math.random() + (extension || ""); + const file = new FileUtils.File(PathUtils.join(PathUtils.tempDir, name)); + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0755", 8)); + + registerCleanupFunction(() => { + file.remove(false); + }); + + return file; +} diff --git a/devtools/shared/tests/xpcshell/test_fetch-http.js b/devtools/shared/tests/xpcshell/test_fetch-http.js new file mode 100644 index 0000000000..deb9c7b8f6 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_fetch-http.js @@ -0,0 +1,69 @@ +/* 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 } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); + +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) { + info("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); + + const body = "[" + Math.random() + "]"; + response.bodyOutputStream.write(body, body.length); +} + +do_get_profile(); + +registerCleanupFunction(() => { + return new Promise(resolve => server.stop(resolve)); +}); + +add_task(async function test_normal() { + await DevToolsUtils.fetch(NORMAL_URL).then(({ content }) => { + ok( + content.includes("The content looks correct."), + "The content looks correct." + ); + }); +}); + +add_task(async function test_caching() { + let initialContent = null; + + info("Performing the first request."); + await DevToolsUtils.fetch(CACHED_URL).then(({ content }) => { + info("Got the first response: " + content); + initialContent = content; + }); + + info("Performing another request, expecting to get cached response."); + await DevToolsUtils.fetch(CACHED_URL).then(({ content }) => { + deepEqual(content, initialContent, "The content was loaded from cache."); + }); + + info("Performing a third request with cache bypassed."); + const opts = { loadFromCache: false }; + await 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/xpcshell/test_fetch-resource.js b/devtools/shared/tests/xpcshell/test_fetch-resource.js new file mode 100644 index 0000000000..a320d63151 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_fetch-resource.js @@ -0,0 +1,43 @@ +/* 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"; + +// Disable `xpc::IsInAutomation()` so we don't crash when accessing a +// nonexistent resource URI. +Services.prefs.setBoolPref( + "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer", + false +); + +/** + * Test that non-existent files are handled correctly. + */ +add_task(async function test_missing() { + await DevToolsUtils.fetch(URL_NOT_FOUND).then( + result => { + info(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(async function test_normal() { + await DevToolsUtils.fetch(URL_FOUND).then(result => { + notDeepEqual( + result.content, + "", + "resource:// URI seems to be read correctly." + ); + }); +}); diff --git a/devtools/shared/tests/xpcshell/test_flatten.js b/devtools/shared/tests/xpcshell/test_flatten.js new file mode 100644 index 0000000000..2e2a80a9d5 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_flatten.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// 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/xpcshell/test_indentation.js b/devtools/shared/tests/xpcshell/test_indentation.js new file mode 100644 index 0000000000..7ed84c6a87 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_indentation.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + EXPAND_TAB, + TAB_SIZE, + DETECT_INDENT, + getTabPrefs, + getIndentationFromPrefs, + getIndentationFromIteration, + getIndentationFromString, +} = require("resource://devtools/shared/indentation.js"); + +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( + getTabPrefs(), + { indentUnit: 73, indentWithTabs: true }, + "getTabPrefs basic test" + ); + 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 (const test of TESTS) { + const iterFn = function (start, end, callback) { + test.input.slice(start, end).forEach(callback); + }; + + deepEqual( + getIndentationFromIteration(iterFn), + test.expected, + "test getIndentationFromIteration " + test.desc + ); + } + + for (const 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/xpcshell/test_independent_loaders.js b/devtools/shared/tests/xpcshell/test_independent_loaders.js new file mode 100644 index 0000000000..ee8771db25 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_independent_loaders.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * 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() { + const loader1 = new DevToolsLoader(); + const loader2 = new DevToolsLoader(); + + const indent1 = loader1.require("resource://devtools/shared/indentation.js"); + const indent2 = loader2.require("resource://devtools/shared/indentation.js"); + + Assert.ok(indent1 !== indent2); + + Assert.ok(loader1.loader !== loader2.loader); + Assert.ok(loader1.id !== loader2.id); +} diff --git a/devtools/shared/tests/xpcshell/test_invisible_loader.js b/devtools/shared/tests/xpcshell/test_invisible_loader.js new file mode 100644 index 0000000000..d83efd9c2a --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_invisible_loader.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" +); +addDebuggerToGlobal(globalThis); + +/** + * Ensure that sandboxes created via the Dev Tools loader respect the + * invisibleToDebugger flag. + */ +function run_test() { + visible_loader(); + invisible_loader(); + // TODO: invisibleToDebugger should be deprecated in favor of + // useDistinctSystemPrincipalLoader, but we might move out from the loader + // to using only standard imports instead. + distinct_system_principal_loader(); +} + +function visible_loader() { + const loader = new DevToolsLoader({ + invisibleToDebugger: false, + }); + loader.require("resource://devtools/shared/indentation.js"); + + const dbg = new Debugger(); + const sandbox = loader.loader.sharedGlobal; + + try { + dbg.addDebuggee(sandbox); + Assert.ok(true); + } catch (e) { + do_throw("debugger could not add visible value"); + } +} + +function invisible_loader() { + const loader = new DevToolsLoader({ + invisibleToDebugger: true, + }); + loader.require("resource://devtools/shared/indentation.js"); + + const dbg = new Debugger(); + const sandbox = loader.loader.sharedGlobal; + + try { + dbg.addDebuggee(sandbox); + do_throw("debugger added invisible value"); + } catch (e) { + Assert.ok(true); + } +} + +function distinct_system_principal_loader() { + const { + useDistinctSystemPrincipalLoader, + releaseDistinctSystemPrincipalLoader, + } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs" + ); + + const requester = {}; + const loader = useDistinctSystemPrincipalLoader(requester); + loader.require("resource://devtools/shared/indentation.js"); + + const dbg = new Debugger(); + const sandbox = loader.loader.sharedGlobal; + + try { + dbg.addDebuggee(sandbox); + do_throw("debugger added invisible value"); + } catch (e) { + Assert.ok(true); + } + releaseDistinctSystemPrincipalLoader(requester); +} diff --git a/devtools/shared/tests/xpcshell/test_isSet.js b/devtools/shared/tests/xpcshell/test_isSet.js new file mode 100644 index 0000000000..73ccb6fe6a --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_isSet.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test ThreadSafeDevToolsUtils.isSet + +function run_test() { + Services.prefs.setBoolPref( + "security.allow_parent_unrestricted_js_loads", + true + ); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads"); + }); + + 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/xpcshell/test_loader.js b/devtools/shared/tests/xpcshell/test_loader.js new file mode 100644 index 0000000000..56ee6459d3 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_loader.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + useDistinctSystemPrincipalLoader, + releaseDistinctSystemPrincipalLoader, +} = ChromeUtils.importESModule( + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs" +); + +function run_test() { + const requester = {}, + requester2 = {}; + + const loader = useDistinctSystemPrincipalLoader(requester); + + // The DevTools dedicated global forces invisibleToDebugger on its realm at + // https://searchfox.org/mozilla-central/rev/12a18f7e112a4dcf88d8441d439b84144bfbe9a3/js/xpconnect/loader/mozJSModuleLoader.cpp#591-593 + // but this is not observable directly. + const DevToolsSpecialGlobal = Cu.getGlobalForObject( + ChromeUtils.importESModule( + "resource://devtools/shared/DevToolsInfaillibleUtils.sys.mjs", + { loadInDevToolsLoader: true } + ) + ); + + const regularLoader = new DevToolsLoader(); + Assert.notStrictEqual( + DevToolsSpecialGlobal, + regularLoader.loader.sharedGlobal, + "The regular loader is not using the special DevTools global" + ); + + info("Assert the key difference with the other regular loaders:"); + Assert.strictEqual( + DevToolsSpecialGlobal, + loader.loader.sharedGlobal, + "The system principal loader is using the special DevTools global" + ); + + ok(loader.loader, "Loader is not destroyed before calling release"); + + info("Now assert the precise behavior of release"); + releaseDistinctSystemPrincipalLoader({}); + ok( + loader.loader, + "Loader is still not destroyed after calling release with another requester" + ); + + releaseDistinctSystemPrincipalLoader(requester); + ok( + !loader.loader, + "Loader is destroyed after calling release with the right requester" + ); + + info("Now test the behavior with two concurrent usages"); + const loader2 = useDistinctSystemPrincipalLoader(requester); + Assert.notEqual(loader, loader2, "We get a new loader instance"); + Assert.strictEqual( + DevToolsSpecialGlobal, + loader2.loader.sharedGlobal, + "The new system principal loader is also using the special DevTools global" + ); + + const loader3 = useDistinctSystemPrincipalLoader(requester2); + Assert.equal(loader2, loader3, "The two loader last loaders are shared"); + + releaseDistinctSystemPrincipalLoader(requester); + ok(loader2.loader, "Loader isn't destroy on the first call to destroy"); + + releaseDistinctSystemPrincipalLoader(requester2); + ok(!loader2.loader, "Loader is destroyed on the second call to destroy"); +} diff --git a/devtools/shared/tests/xpcshell/test_natural-sort.js b/devtools/shared/tests/xpcshell/test_natural-sort.js new file mode 100644 index 0000000000..d01b984c73 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_natural-sort.js @@ -0,0 +1,959 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const l10n = new Localization(["devtools/client/storage.ftl"], true); +const sessionString = l10n.formatValueSync("storage-expires-session"); +const { + naturalSortCaseSensitive, + naturalSortCaseInsensitive, +} = require("resource://devtools/shared/natural-sort.js"); + +function run_test() { + test("different values types", function () { + runTest(["a", 1], [1, "a"], "number always comes first"); + runTest( + ["1", 1], + ["1", 1], + "number vs numeric string - should remain unchanged (error in chrome)" + ); + runTest( + ["02", 3, 2, "01"], + ["01", "02", 2, 3], + "padding numeric string vs number" + ); + }); + + test("datetime", function () { + runTest( + ["10/12/2008", "10/11/2008", "10/11/2007", "10/12/2007"], + ["10/11/2007", "10/12/2007", "10/11/2008", "10/12/2008"], + "similar dates" + ); + runTest( + ["01/01/2008", "01/10/2008", "01/01/1992", "01/01/1991"], + ["01/01/1991", "01/01/1992", "01/01/2008", "01/10/2008"], + "similar dates" + ); + // Years should expand to 0100, 2001, 2010 + runTest( + ["1/1/100", "1/1/1", "1/1/10"], + ["1/1/100", "1/1/1", "1/1/10"], + "dates with short year formatting" + ); + runTest( + [ + "Wed Jan 01 2010 00:00:00 GMT-0800 (Pacific Standard Time)", + "Thu Dec 31 2009 00:00:00 GMT-0800 (Pacific Standard Time)", + "Wed Jan 01 2010 00:00:00 GMT-0500 (Eastern Standard Time)", + ], + [ + "Thu Dec 31 2009 00:00:00 GMT-0800 (Pacific Standard Time)", + "Wed Jan 01 2010 00:00:00 GMT-0500 (Eastern Standard Time)", + "Wed Jan 01 2010 00:00:00 GMT-0800 (Pacific Standard Time)", + ], + "javascript toString(), different timezones" + ); + runTest( + [ + "Saturday, July 3, 2010", + "Monday, August 2, 2010", + "Monday, May 3, 2010", + ], + [ + "Monday, May 3, 2010", + "Saturday, July 3, 2010", + "Monday, August 2, 2010", + ], + "Date.toString(), Date.toLocaleString()" + ); + runTest( + [ + "Mon, 15 Jun 2009 20:45:30 GMT", + "Mon, 3 May 2010 17:45:30 GMT", + "Mon, 15 Jun 2009 17:45:30 GMT", + ], + [ + "Mon, 15 Jun 2009 17:45:30 GMT", + "Mon, 15 Jun 2009 20:45:30 GMT", + "Mon, 3 May 2010 17:45:30 GMT", + ], + "Date.toUTCString()" + ); + runTest( + [ + "Saturday, July 3, 2010 1:45 PM", + "Saturday, July 3, 2010 1:45 AM", + "Monday, August 2, 2010 1:45 PM", + "Monday, May 3, 2010 1:45 PM", + ], + [ + "Monday, May 3, 2010 1:45 PM", + "Saturday, July 3, 2010 1:45 AM", + "Saturday, July 3, 2010 1:45 PM", + "Monday, August 2, 2010 1:45 PM", + ], + "" + ); + runTest( + [ + "Saturday, July 3, 2010 1:45:30 PM", + "Saturday, July 3, 2010 1:45:29 PM", + "Monday, August 2, 2010 1:45:01 PM", + "Monday, May 3, 2010 1:45:00 PM", + ], + [ + "Monday, May 3, 2010 1:45:00 PM", + "Saturday, July 3, 2010 1:45:29 PM", + "Saturday, July 3, 2010 1:45:30 PM", + "Monday, August 2, 2010 1:45:01 PM", + ], + "" + ); + runTest( + ["2/15/2009 1:45 PM", "1/15/2009 1:45 PM", "2/15/2009 1:45 AM"], + ["1/15/2009 1:45 PM", "2/15/2009 1:45 AM", "2/15/2009 1:45 PM"], + "" + ); + runTest( + [ + "2010-06-15T13:45:30", + "2009-06-15T13:45:30", + "2009-06-15T01:45:30.2", + "2009-01-15T01:45:30", + ], + [ + "2009-01-15T01:45:30", + "2009-06-15T01:45:30.2", + "2009-06-15T13:45:30", + "2010-06-15T13:45:30", + ], + "ISO8601 Dates" + ); + runTest( + ["2010-06-15 13:45:30", "2009-06-15 13:45:30", "2009-01-15 01:45:30"], + ["2009-01-15 01:45:30", "2009-06-15 13:45:30", "2010-06-15 13:45:30"], + "ISO8601-ish YYYY-MM-DDThh:mm:ss - which does not parse into a Date instance" + ); + runTest( + [ + "Mon, 15 Jun 2009 20:45:30 GMT", + "Mon, 15 Jun 2009 20:45:30 PDT", + "Mon, 15 Jun 2009 20:45:30 EST", + ], + [ + "Mon, 15 Jun 2009 20:45:30 GMT", + "Mon, 15 Jun 2009 20:45:30 EST", + "Mon, 15 Jun 2009 20:45:30 PDT", + ], + "RFC1123 testing different timezones" + ); + runTest( + ["1245098730000", "14330728000", "1245098728000"], + ["14330728000", "1245098728000", "1245098730000"], + "unix epoch, Date.getTime()" + ); + runTest( + [ + new Date("2001-01-10"), + "2015-01-01", + new Date("2001-01-01"), + "1998-01-01", + ], + [ + "1998-01-01", + new Date("2001-01-01"), + new Date("2001-01-10"), + "2015-01-01", + ], + "mixed Date types" + ); + runTest( + [ + "Tue, 29 Jun 2021 11:31:17 GMT", + "Sun, 14 Jun 2009 11:11:15 GMT", + sessionString, + "Mon, 15 Jun 2009 20:45:30 GMT", + ], + [ + sessionString, + "Sun, 14 Jun 2009 11:11:15 GMT", + "Mon, 15 Jun 2009 20:45:30 GMT", + "Tue, 29 Jun 2021 11:31:17 GMT", + ], + `"${sessionString}" amongst date strings` + ); + runTest( + [ + "Wed, 04 Sep 2024 09:11:44 GMT", + sessionString, + "Tue, 06 Sep 2022 09:11:44 GMT", + sessionString, + "Mon, 05 Sep 2022 09:12:41 GMT", + ], + [ + sessionString, + sessionString, + "Mon, 05 Sep 2022 09:12:41 GMT", + "Tue, 06 Sep 2022 09:11:44 GMT", + "Wed, 04 Sep 2024 09:11:44 GMT", + ], + `"${sessionString}" amongst date strings (complex)` + ); + + runTest( + [ + "Madras", + "Jalfrezi", + "Rogan Josh", + "Vindaloo", + "Tikka Masala", + sessionString, + "Masala", + "Korma", + ], + [ + "Jalfrezi", + "Korma", + "Madras", + "Masala", + "Rogan Josh", + sessionString, + "Tikka Masala", + "Vindaloo", + ], + `"${sessionString}" amongst strings` + ); + }); + + test("version number strings", function () { + runTest( + ["1.0.2", "1.0.1", "1.0.0", "1.0.9"], + ["1.0.0", "1.0.1", "1.0.2", "1.0.9"], + "close version numbers" + ); + runTest( + ["1.1.100", "1.1.1", "1.1.10", "1.1.54"], + ["1.1.1", "1.1.10", "1.1.54", "1.1.100"], + "more version numbers" + ); + runTest( + ["1.0.03", "1.0.003", "1.0.002", "1.0.0001"], + ["1.0.0001", "1.0.002", "1.0.003", "1.0.03"], + "multi-digit branch release" + ); + runTest( + [ + "1.1beta", + "1.1.2alpha3", + "1.0.2alpha3", + "1.0.2alpha1", + "1.0.1alpha4", + "2.1.2", + "2.1.1", + ], + [ + "1.0.1alpha4", + "1.0.2alpha1", + "1.0.2alpha3", + "1.1.2alpha3", + "1.1beta", + "2.1.1", + "2.1.2", + ], + "close version numbers" + ); + runTest( + [ + "myrelease-1.1.3", + "myrelease-1.2.3", + "myrelease-1.1.4", + "myrelease-1.1.1", + "myrelease-1.0.5", + ], + [ + "myrelease-1.0.5", + "myrelease-1.1.1", + "myrelease-1.1.3", + "myrelease-1.1.4", + "myrelease-1.2.3", + ], + "string first" + ); + runTest( + [ + "1.1.3", + "a-release-1.1.3", + "b-release-1.1.3", + "1.2.3", + "a-release-1.2.3", + "b-release-1.2.3", + "1.1.4", + "a-release-1.1.4", + "b-release-1.1.4", + "1.1.1", + "a-release-1.1.1", + "b-release-1.1.1", + "1.0.5", + "a-release-1.0.5", + "b-release-1.0.5", + ], + [ + "1.0.5", + "1.1.1", + "1.1.3", + "1.1.4", + "1.2.3", + "a-release-1.0.5", + "a-release-1.1.1", + "a-release-1.1.3", + "a-release-1.1.4", + "a-release-1.2.3", + "b-release-1.0.5", + "b-release-1.1.1", + "b-release-1.1.3", + "b-release-1.1.4", + "b-release-1.2.3", + ], + "string first, different names" + ); + runTest( + ["zstring", "astring", "release-1.1.3"], + ["astring", "release-1.1.3", "zstring"], + "string first, mixed with regular strings" + ); + }); + + test("numerics", function () { + runTest(["10", 9, 2, "1", "4"], ["1", 2, "4", 9, "10"], "string vs number"); + runTest( + ["0001", "002", "001"], + ["0001", "001", "002"], + "0 left-padded numbers" + ); + runTest( + [2, 1, "1", "0001", "002", "02", "001"], + [1, "1", "0001", "001", 2, "002", "02"], + "0 left-padded numbers and regular numbers" + ); + runTest( + ["10.0401", 10.022, 10.042, "10.021999"], + ["10.021999", 10.022, "10.0401", 10.042], + "decimal string vs decimal, different precision" + ); + runTest( + ["10.04", 10.02, 10.03, "10.01"], + ["10.01", 10.02, 10.03, "10.04"], + "decimal string vs decimal, same precision" + ); + runTest( + ["10.04f", "10.039F", "10.038d", "10.037D"], + ["10.037D", "10.038d", "10.039F", "10.04f"], + "float/decimal with 'F' or 'D' notation" + ); + runTest( + ["10.004Z", "10.039T", "10.038ooo", "10.037g"], + ["10.004Z", "10.037g", "10.038ooo", "10.039T"], + "not foat/decimal notation" + ); + runTest( + ["1.528535047e5", "1.528535047e7", "1.52e15", "1.528535047e3", "1.59e-3"], + ["1.59e-3", "1.528535047e3", "1.528535047e5", "1.528535047e7", "1.52e15"], + "scientific notation" + ); + runTest( + ["-1", "-2", "4", "-3", "0", "-5"], + ["-5", "-3", "-2", "-1", "0", "4"], + "negative numbers as strings" + ); + runTest( + [-1, "-2", 4, -3, "0", "-5"], + ["-5", -3, "-2", -1, "0", 4], + "negative numbers as strings - mixed input type, string + numeric" + ); + runTest( + [-2.01, -2.1, 4.144, 4.1, -2.001, -5], + [-5, -2.1, -2.01, -2.001, 4.1, 4.144], + "negative floats - all numeric" + ); + }); + + test("IP addresses", function () { + runTest( + [ + "192.168.0.100", + "192.168.0.1", + "192.168.1.1", + "192.168.0.250", + "192.168.1.123", + "10.0.0.2", + "10.0.0.1", + ], + [ + "10.0.0.1", + "10.0.0.2", + "192.168.0.1", + "192.168.0.100", + "192.168.0.250", + "192.168.1.1", + "192.168.1.123", + ] + ); + }); + + test("filenames", function () { + runTest( + ["img12.png", "img10.png", "img2.png", "img1.png"], + ["img1.png", "img2.png", "img10.png", "img12.png"], + "simple image filenames" + ); + runTest( + [ + "car.mov", + "01alpha.sgi", + "001alpha.sgi", + "my.string_41299.tif", + "organic2.0001.sgi", + ], + [ + "001alpha.sgi", + "01alpha.sgi", + "car.mov", + "my.string_41299.tif", + "organic2.0001.sgi", + ], + "complex filenames" + ); + runTest( + [ + "./system/kernel/js/01_ui.core.js", + "./system/kernel/js/00_jquery-1.3.2.js", + "./system/kernel/js/02_my.desktop.js", + ], + [ + "./system/kernel/js/00_jquery-1.3.2.js", + "./system/kernel/js/01_ui.core.js", + "./system/kernel/js/02_my.desktop.js", + ], + "unix filenames" + ); + }); + + test("space(s) as first character(s)", function () { + runTest(["alpha", " 1", " 3", " 2", 0], [0, " 1", " 2", " 3", "alpha"]); + }); + + test("empty strings and space character", function () { + runTest( + ["10023", "999", "", 2, 5.663, 5.6629], + ["", 2, 5.6629, 5.663, "999", "10023"] + ); + runTest([0, "0", ""], [0, "0", ""]); + }); + + test("hex", function () { + runTest(["0xA", "0x9", "0x99"], ["0x9", "0xA", "0x99"], "real hex numbers"); + runTest( + ["0xZZ", "0xVVV", "0xVEV", "0xUU"], + ["0xUU", "0xVEV", "0xVVV", "0xZZ"], + "fake hex numbers" + ); + }); + + test("unicode", function () { + runTest( + ["\u0044", "\u0055", "\u0054", "\u0043"], + ["\u0043", "\u0044", "\u0054", "\u0055"], + "basic latin" + ); + }); + + test("sparse array sort", function () { + const sarray = [3, 2]; + const sarrayOutput = [1, 2, 3]; + + sarray[10] = 1; + for (let i = 0; i < 8; i++) { + sarrayOutput.push(undefined); + } + runTest(sarray, sarrayOutput, "simple sparse array"); + }); + + test("case insensitive support", function () { + runTest( + ["A", "b", "C", "d", "E", "f"], + ["A", "b", "C", "d", "E", "f"], + "case sensitive pre-sorted array", + true + ); + runTest( + ["A", "C", "E", "b", "d", "f"], + ["A", "b", "C", "d", "E", "f"], + "case sensitive un-sorted array", + true + ); + runTest( + ["A", "C", "E", "b", "d", "f"], + ["A", "C", "E", "b", "d", "f"], + "case sensitive pre-sorted array" + ); + runTest( + ["A", "b", "C", "d", "E", "f"], + ["A", "C", "E", "b", "d", "f"], + "case sensitive un-sorted array" + ); + }); + + test("rosetta code natural sort small test set", function () { + runTest( + [ + "ignore leading spaces: 2-2", + " ignore leading spaces: 2-1", + " ignore leading spaces: 2+0", + " ignore leading spaces: 2+1", + ], + [ + " ignore leading spaces: 2+0", + " ignore leading spaces: 2+1", + " ignore leading spaces: 2-1", + "ignore leading spaces: 2-2", + ], + "Ignoring leading spaces" + ); + runTest( + [ + "ignore m.a.s spaces: 2-2", + "ignore m.a.s spaces: 2-1", + "ignore m.a.s spaces: 2+0", + "ignore m.a.s spaces: 2+1", + ], + [ + "ignore m.a.s spaces: 2+0", + "ignore m.a.s spaces: 2+1", + "ignore m.a.s spaces: 2-1", + "ignore m.a.s spaces: 2-2", + ], + "Ignoring multiple adjacent spaces (m.a.s)" + ); + runTest( + [ + "Equiv. spaces: 3-3", + "Equiv.\rspaces: 3-2", + "Equiv.\x0cspaces: 3-1", + "Equiv.\x0bspaces: 3+0", + "Equiv.\nspaces: 3+1", + "Equiv.\tspaces: 3+2", + ], + [ + "Equiv.\x0bspaces: 3+0", + "Equiv.\nspaces: 3+1", + "Equiv.\tspaces: 3+2", + "Equiv.\x0cspaces: 3-1", + "Equiv.\rspaces: 3-2", + "Equiv. spaces: 3-3", + ], + "Equivalent whitespace characters" + ); + runTest( + [ + "cASE INDEPENENT: 3-2", + "caSE INDEPENENT: 3-1", + "casE INDEPENENT: 3+0", + "case INDEPENENT: 3+1", + ], + [ + "casE INDEPENENT: 3+0", + "case INDEPENENT: 3+1", + "caSE INDEPENENT: 3-1", + "cASE INDEPENENT: 3-2", + ], + "Case Indepenent sort (naturalSort.insensitive = true)", + true + ); + runTest( + [ + "foo100bar99baz0.txt", + "foo100bar10baz0.txt", + "foo1000bar99baz10.txt", + "foo1000bar99baz9.txt", + ], + [ + "foo100bar10baz0.txt", + "foo100bar99baz0.txt", + "foo1000bar99baz9.txt", + "foo1000bar99baz10.txt", + ], + "Numeric fields as numerics" + ); + runTest( + [ + "The Wind in the Willows", + "The 40th step more", + "The 39 steps", + "Wanda", + ], + [ + "The 39 steps", + "The 40th step more", + "The Wind in the Willows", + "Wanda", + ], + "Title sorts" + ); + runTest( + [ + "Equiv. \xfd accents: 2-2", + "Equiv. \xdd accents: 2-1", + "Equiv. y accents: 2+0", + "Equiv. Y accents: 2+1", + ], + [ + "Equiv. y accents: 2+0", + "Equiv. Y accents: 2+1", + "Equiv. \xfd accents: 2-2", + "Equiv. \xdd accents: 2-1", + ], + "Equivalent accented characters (and case) (naturalSort.insensitive = true)", + true + ); + // This is not a valuable unicode ordering test + // runTest( + // ['Start with an \u0292: 2-2', 'Start with an \u017f: 2-1', 'Start with an \xdf: 2+0', 'Start with an s: 2+1'], + // ['Start with an s: 2+1', 'Start with an \xdf: 2+0', 'Start with an \u017f: 2-1', 'Start with an \u0292: 2-2'], + // 'Character replacements'); + }); + + test("contributed tests", function () { + runTest( + [ + "T78", + "U17", + "U10", + "U12", + "U14", + "745", + "U7", + "485", + "S16", + "S2", + "S22", + "1081", + "S25", + "1055", + "779", + "776", + "771", + "44", + "4", + "87", + "1091", + "42", + "480", + "952", + "951", + "756", + "1000", + "824", + "770", + "666", + "633", + "619", + "1", + "991", + "77H", + "PIER-7", + "47", + "29", + "9", + "77L", + "433", + ], + [ + "1", + "4", + "9", + "29", + "42", + "44", + "47", + "77H", + "77L", + "87", + "433", + "480", + "485", + "619", + "633", + "666", + "745", + "756", + "770", + "771", + "776", + "779", + "824", + "951", + "952", + "991", + "1000", + "1055", + "1081", + "1091", + "PIER-7", + "S2", + "S16", + "S22", + "S25", + "T78", + "U7", + "U10", + "U12", + "U14", + "U17", + ], + "contributed by Bob Zeiner (Chrome not stable sort)" + ); + runTest( + [ + "FSI stop, Position: 5", + "Mail Group stop, Position: 5", + "Mail Group stop, Position: 5", + "FSI stop, Position: 6", + "FSI stop, Position: 6", + "Newsstand stop, Position: 4", + "Newsstand stop, Position: 4", + "FSI stop, Position: 5", + ], + [ + "FSI stop, Position: 5", + "FSI stop, Position: 5", + "FSI stop, Position: 6", + "FSI stop, Position: 6", + "Mail Group stop, Position: 5", + "Mail Group stop, Position: 5", + "Newsstand stop, Position: 4", + "Newsstand stop, Position: 4", + ], + "contributed by Scott" + ); + runTest( + [2, 10, 1, "azd", undefined, "asd"], + [1, 2, 10, "asd", "azd", undefined], + "issue #2 - undefined support - jarvinen pekka" + ); + runTest( + [undefined, undefined, undefined, 1, undefined], + [1, undefined, undefined, undefined], + "issue #2 - undefined support - jarvinen pekka" + ); + runTest( + ["-1", "-2", "4", "-3", "0", "-5"], + ["-5", "-3", "-2", "-1", "0", "4"], + "issue #3 - invalid numeric string sorting - guilermo.dev" + ); + // native sort implementations are not guaranteed to be stable (i.e. Chrome) + // runTest( + // ['9','11','22','99','A','aaaa','bbbb','Aaaa','aAaa','aa','AA','Aa','aA','BB','bB','aaA','AaA','aaa'], + // ['9', '11', '22', '99', 'A', 'aa', 'AA', 'Aa', 'aA', 'aaA', 'AaA', 'aaa', 'aaaa', 'Aaaa', 'aAaa', 'BB', 'bB', 'bbbb'], + // 'issue #5 - invalid sort order - Howie Schecter (naturalSort.insensitive = true)'m true); + runTest( + [ + "9", + "11", + "22", + "99", + "A", + "aaaa", + "bbbb", + "Aaaa", + "aAaa", + "aa", + "AA", + "Aa", + "aA", + "BB", + "bB", + "aaA", + "AaA", + "aaa", + ], + [ + "9", + "11", + "22", + "99", + "A", + "AA", + "Aa", + "AaA", + "Aaaa", + "BB", + "aA", + "aAaa", + "aa", + "aaA", + "aaa", + "aaaa", + "bB", + "bbbb", + ], + "issue #5 - invalid sort order - Howie Schecter (naturalSort.insensitive = false)" + ); + runTest( + [ + "5D", + "1A", + "2D", + "33A", + "5E", + "33K", + "33D", + "5S", + "2C", + "5C", + "5F", + "1D", + "2M", + ], + [ + "1A", + "1D", + "2C", + "2D", + "2M", + "5C", + "5D", + "5E", + "5F", + "5S", + "33A", + "33D", + "33K", + ], + "alphanumeric - number first" + ); + runTest( + ["img 99", "img199", "imga99", "imgz99"], + ["img 99", "img199", "imga99", "imgz99"], + "issue #16 - Sorting incorrect when there is a space - adrien-be" + ); + runTest( + ["img199", "img 99", "imga99", "imgz 99", "imgb99", "imgz199"], + ["img 99", "img199", "imga99", "imgb99", "imgz 99", "imgz199"], + "issue #16 - expanded test" + ); + runTest( + ["1", "02", "3"], + ["1", "02", "3"], + "issue #18 - Any zeros that precede a number messes up the sorting - menixator" + ); + // strings are coerced as floats/ints if possible and sorted accordingly - e.g. they are not chunked + runTest( + ["1.100", "1.1", "1.10", "1.54"], + ["1.100", "1.1", "1.10", "1.54"], + "issue #13 - ['1.100', '1.10', '1.1', '1.54'] etc do not sort properly... - rubenstolk" + ); + runTest( + ["v1.100", "v1.1", "v1.10", "v1.54"], + ["v1.1", "v1.10", "v1.54", "v1.100"], + "issue #13 - ['v1.100', 'v1.10', 'v1.1', 'v1.54'] etc do not sort properly... - rubenstolk (bypass float coercion)" + ); + runTest( + [ + "MySnmp 1234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567", + "MySnmp 4234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567", + "MySnmp 2234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567", + "MySnmp 3234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567", + ], + [ + "MySnmp 1234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567", + "MySnmp 2234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567", + "MySnmp 3234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567", + "MySnmp 4234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567891234567", + ], + "issue #14 - Very large numbers make sorting very slow - Mottie" + ); + runTest( + ["bar.1-2", "bar.1"], + ["bar.1", "bar.1-2"], + "issue #21 - javascript error" + ); + runTest( + ["SomeString", "SomeString 1"], + ["SomeString", "SomeString 1"], + "PR #19 - ['SomeString', 'SomeString 1'] bombing on 'undefined is not an object' - dannycochran" + ); + runTest( + [ + "Udet", + "\xDCbelacker", + "Uell", + "\xDClle", + "Ueve", + "\xDCxk\xFCll", + "Uffenbach", + ], + [ + "\xDCbelacker", + "Udet", + "Uell", + "Ueve", + "Uffenbach", + "\xDClle", + "\xDCxk\xFCll", + ], + "issue #9 - Sorting umlauts characters \xC4, \xD6, \xDC - diogoalves" + ); + runTest( + ["2.2 sec", "1.9 sec", "1.53 sec"], + ["1.53 sec", "1.9 sec", "2.2 sec"], + "https://github.com/overset/javascript-natural-sort/issues/13 - ['2.2 sec','1.9 sec','1.53 sec'] - padded by spaces - harisb" + ); + runTest( + ["2.2sec", "1.9sec", "1.53sec"], + ["1.53sec", "1.9sec", "2.2sec"], + "https://github.com/overset/javascript-natural-sort/issues/13 - ['2.2sec','1.9sec','1.53sec'] - no padding - harisb" + ); + }); +} + +function test(description, testFunc) { + info(description); + testFunc(); +} + +function runTest(testArray, expected, description, caseInsensitive = false) { + let actual = null; + + if (caseInsensitive) { + actual = testArray.sort((a, b) => + naturalSortCaseInsensitive(a, b, sessionString) + ); + } else { + actual = testArray.sort((a, b) => + naturalSortCaseSensitive(a, b, sessionString) + ); + } + + compareOptions(actual, expected, description); +} + +// deepEqual() doesn't work well for testing arrays containing `undefined` so +// we need to use a custom method. +function compareOptions(actual, expected, description) { + let match = true; + for (let i = 0; i < actual.length; i++) { + if (actual[i] + "" !== expected[i] + "") { + ok( + false, + `${description}\nElement ${i} does not match:\n[${i}] ${actual[i]}\n[${i}] ${expected[i]}` + ); + match = false; + break; + } + } + if (match) { + ok(true, description); + } +} diff --git a/devtools/shared/tests/xpcshell/test_pluralForm-english.js b/devtools/shared/tests/xpcshell/test_pluralForm-english.js new file mode 100644 index 0000000000..2d2f29d47e --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_pluralForm-english.js @@ -0,0 +1,32 @@ +/* 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("resource://devtools/shared/plural-form.js"); + +function run_test() { + // English has 2 plural forms + Assert.equal(2, PluralForm.numForms()); + + // Make sure for good inputs, things work as expected + for (let num = 0; num <= 200; num++) { + Assert.equal( + num == 1 ? "word" : "words", + PluralForm.get(num, "word;words") + ); + } + + // Not having enough plural forms defaults to the first form + Assert.equal("word", PluralForm.get(2, "word")); + + // Empty forms defaults to the first form + Assert.equal("word", PluralForm.get(2, "word;")); +} diff --git a/devtools/shared/tests/xpcshell/test_pluralForm-makeGetter.js b/devtools/shared/tests/xpcshell/test_pluralForm-makeGetter.js new file mode 100644 index 0000000000..d9d1facca2 --- /dev/null +++ b/devtools/shared/tests/xpcshell/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("resource://devtools/shared/plural-form.js"); + +function run_test() { + // Irish is plural rule #11 + const [get, numForms] = PluralForm.makeGetter(11); + + // Irish has 5 plural forms + Assert.equal(5, numForms()); + + // I don't really know Irish, so I'll stick in some dummy text + const words = "is 1;is 2;is 3-6;is 7-10;everything else"; + + const test = function (text, low, high) { + for (let num = low; num <= high; num++) { + Assert.equal(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/xpcshell/test_prettifyCSS.js b/devtools/shared/tests/xpcshell/test_prettifyCSS.js new file mode 100644 index 0000000000..1839c6a253 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_prettifyCSS.js @@ -0,0 +1,195 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test prettifyCSS. + +"use strict"; + +const { + prettifyCSS, +} = require("resource://devtools/shared/inspector/css-logic.js"); + +const EXPAND_TAB = "devtools.editor.expandtab"; +const TAB_SIZE = "devtools.editor.tabsize"; + +const TESTS_TAB_INDENT = [ + { + name: "simple test. indent using tabs", + input: "div { font-family:'Arial Black', Arial, sans-serif; }", + expected: ["div {", "\tfont-family:'Arial Black', Arial, sans-serif;", "}"], + }, + + { + name: "whitespace before open brace. indent using tabs", + input: "div{}", + expected: ["div {", "}"], + }, + + { + name: "minified with trailing newline. indent using tabs", + 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. indent using tabs", + input: "\n div{color: red;}", + expected: ["div {", "\tcolor: red;", "}"], + }, + + { + name: "CSS with extra closing brace. indent using tabs", + input: "body{margin:0}} div{color:red}", + expected: ["body {", "\tmargin:0", "}", "}", "div {", "\tcolor:red", "}"], + }, +]; + +const TESTS_SPACE_INDENT = [ + { + name: "simple test. indent using spaces", + input: "div { font-family:'Arial Black', Arial, sans-serif; }", + expected: [ + "div {", + " font-family:'Arial Black', Arial, sans-serif;", + "}", + ], + }, + + { + name: "whitespace before open brace. indent using spaces", + input: "div{}", + expected: ["div {", "}"], + }, + + { + name: "minified with trailing newline. indent using spaces", + input: + "\nbody{background:white;}div{font-size:4em;color:red}span{color:green;}\n", + expected: [ + "body {", + " background:white;", + "}", + "div {", + " font-size:4em;", + " color:red", + "}", + "span {", + " color:green;", + "}", + ], + }, + + { + name: "leading whitespace. indent using spaces", + input: "\n div{color: red;}", + expected: ["div {", " color: red;", "}"], + }, + + { + name: "CSS with extra closing brace. indent using spaces", + input: "body{margin:0}} div{color:red}", + expected: [ + "body {", + " margin:0", + "}", + "}", + "div {", + " color:red", + "}", + ], + }, + + { + name: "HTML comments with some whitespace padding", + input: " \n\n\t <!--\n\n\t body {color:red} \n\n--> \t\n", + expected: ["body {", " color:red", "}"], + }, + + { + name: "HTML comments without whitespace padding", + input: "<!--body {color:red}-->", + expected: ["body {", " color:red", "}"], + }, + + { + name: "Breaking after commas in selectors", + input: + "@media screen, print {div, span, input {color: red;}}" + + "div, div, input, pre, table {color: blue;}", + expected: [ + "@media screen, print {", + " div,", + " span,", + " input {", + " color: red;", + " }", + "}", + "div,", + "div,", + "input,", + "pre,", + "table {", + " color: blue;", + "}", + ], + }, + + { + name: "Multiline comment in CSS", + input: "/*\n * comment\n */\n#example{display:grid;}", + expected: [ + "/*", + " * comment", + " */", + "#example {", + " display:grid;", + "}", + ], + }, +]; + +function run_test() { + // Note that prettifyCSS.LINE_SEPARATOR is computed lazily, so we + // ensure it is set. + prettifyCSS(""); + + Services.prefs.setBoolPref(EXPAND_TAB, true); + Services.prefs.setIntPref(TAB_SIZE, 4); + + for (const test of TESTS_SPACE_INDENT) { + info(test.name); + + const input = test.input.split("\n").join(prettifyCSS.LINE_SEPARATOR); + const { result: output } = prettifyCSS(input); + const expected = + test.expected.join(prettifyCSS.LINE_SEPARATOR) + + prettifyCSS.LINE_SEPARATOR; + equal(output, expected, test.name); + } + + Services.prefs.setBoolPref(EXPAND_TAB, false); + for (const test of TESTS_TAB_INDENT) { + info(test.name); + + const input = test.input.split("\n").join(prettifyCSS.LINE_SEPARATOR); + const { result: output } = prettifyCSS(input); + const expected = + test.expected.join(prettifyCSS.LINE_SEPARATOR) + + prettifyCSS.LINE_SEPARATOR; + equal(output, expected, test.name); + } + Services.prefs.clearUserPref(EXPAND_TAB); + Services.prefs.clearUserPref(TAB_SIZE); +} diff --git a/devtools/shared/tests/xpcshell/test_require.js b/devtools/shared/tests/xpcshell/test_require.js new file mode 100644 index 0000000000..b94aca23e7 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_require.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test require + +// Ensure that DevtoolsLoader.require doesn't spawn multiple +// loader/modules when early cached +function testBug1091706() { + const loader = new DevToolsLoader(); + const require = loader.require; + + const indent1 = require("resource://devtools/shared/indentation.js"); + const indent2 = require("resource://devtools/shared/indentation.js"); + + Assert.ok(indent1 === indent2); +} + +function testInvalidModule() { + const loader = new DevToolsLoader(); + const require = loader.require; + + try { + // This will result in an invalid URL with no scheme and mae loadSubScript + // throws "Error creating URI" error + require("foo"); + Assert.ok(false, "require should throw"); + } catch (error) { + Assert.equal(error.message, "Module `foo` is not found at foo.js"); + Assert.ok( + error.stack.includes("testInvalidModule"), + "Exception's stack includes the test function" + ); + } + + try { + // But when using devtools prefix, the URL is going to be correct but the file + // doesn't exists, leading to "Error opening input stream (invalid filename?)" error + require("resource://devtools/foo.js"); + Assert.ok(false, "require should throw"); + } catch (error) { + Assert.equal( + error.message, + "Module `resource://devtools/foo.js` is not found at resource://devtools/foo.js" + ); + Assert.ok( + error.stack.includes("testInvalidModule"), + "Exception's stack includes the test function" + ); + } +} + +function testThrowingModule() { + const loader = new DevToolsLoader(); + const require = loader.require; + + try { + // Require a test module that is throwing an Error object + require("xpcshell-test/throwing-module-1.js"); + Assert.ok(false, "require should throw"); + } catch (error) { + Assert.equal(error.message, "my-exception"); + Assert.ok( + error.stack.includes("testThrowingModule"), + "Exception's stack includes the test function" + ); + Assert.ok( + error.stack.includes("throwingMethod"), + "Exception's stack also includes the module function that throws" + ); + } + try { + // Require a test module that is throwing a string + require("xpcshell-test/throwing-module-2.js"); + Assert.ok(false, "require should throw"); + } catch (error) { + Assert.equal( + error.message, + "Error while loading module `xpcshell-test/throwing-module-2.js` at " + + "resource://test/throwing-module-2.js:\nmy-exception" + ); + Assert.ok( + error.stack.includes("testThrowingModule"), + "Exception's stack includes the test function" + ); + Assert.ok( + !error.stack.includes("throwingMethod"), + "Exception's stack also includes the module function that throws" + ); + } +} + +function run_test() { + testBug1091706(); + + testInvalidModule(); + + testThrowingModule(); +} diff --git a/devtools/shared/tests/xpcshell/test_require_lazy.js b/devtools/shared/tests/xpcshell/test_require_lazy.js new file mode 100644 index 0000000000..deba7e2128 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_require_lazy.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { loader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +// Test devtools.lazyRequireGetter + +function run_test() { + const name = "asyncUtils"; + const path = "devtools/shared/async-utils"; + const o = {}; + loader.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. + Assert.ok(o.asyncUtils === asyncUtils); + + // A non-main loader should get a new object via |lazyRequireGetter|, just + // as it would via a direct |require|. + const o2 = {}; + const loader2 = new DevToolsLoader(); + + // We have to init the loader by loading any module before + // lazyRequireGetter is available + loader2.require("resource://devtools/shared/DevToolsUtils.js"); + + loader2.lazyRequireGetter(o2, name, path); + Assert.ok(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. + const exposeLoader = loader2.require("xpcshell-test/exposeLoader"); + const o3 = exposeLoader.exerciseLazyRequire(name, path); + Assert.ok(o3.asyncUtils === o2.asyncUtils); +} diff --git a/devtools/shared/tests/xpcshell/test_require_raw.js b/devtools/shared/tests/xpcshell/test_require_raw.js new file mode 100644 index 0000000000..acd0e374b1 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_require_raw.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test require using "raw!". + +function run_test() { + const loader = new DevToolsLoader(); + const require = loader.require; + + const variableFileContents = require("raw!chrome://devtools/skin/variables.css"); + ok(!!variableFileContents.length, "raw browserRequire worked"); + + const propertiesFileContents = require("raw!devtools/client/locales/shared.properties"); + ok( + !!propertiesFileContents.length, + "unprefixed properties raw require worked" + ); + + const chromePropertiesFileContents = require("raw!chrome://devtools/locale/shared.properties"); + ok( + !!chromePropertiesFileContents.length, + "prefixed properties raw require worked" + ); +} diff --git a/devtools/shared/tests/xpcshell/test_safeErrorString.js b/devtools/shared/tests/xpcshell/test_safeErrorString.js new file mode 100644 index 0000000000..1d2e5431ed --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_safeErrorString.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// 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() { + const s = DevToolsUtils.safeErrorString(new Error("foo bar")); + // Got the message. + Assert.ok(s.includes("foo bar")); + // Got the stack. + Assert.ok(s.includes("test_with_error")); + Assert.ok(s.includes("test_safeErrorString.js")); + // Got the lineNumber and columnNumber. + Assert.ok(s.includes("Line")); + Assert.ok(s.includes("column")); +} + +function test_with_tricky_error() { + const e = new Error("batman"); + e.stack = { toString: Object.create(null) }; + const s = DevToolsUtils.safeErrorString(e); + // Still got the message, despite a bad stack property. + Assert.ok(s.includes("batman")); +} + +function test_with_string() { + const s = DevToolsUtils.safeErrorString("not really an error"); + // Still get the message. + Assert.ok(s.includes("not really an error")); +} + +function test_with_thrower() { + const s = DevToolsUtils.safeErrorString({ + toString: () => { + throw new Error("Muahahaha"); + }, + }); + // Still don't fail, get string back. + Assert.equal(typeof s, "string"); +} + +function test_with_psychotic() { + const s = DevToolsUtils.safeErrorString({ + toString: () => Object.create(null), + }); + // Still get a string out, and no exceptions thrown + Assert.equal(typeof s, "string"); + Assert.equal(s, "[object Object]"); +} diff --git a/devtools/shared/tests/xpcshell/test_sprintfjs.js b/devtools/shared/tests/xpcshell/test_sprintfjs.js new file mode 100644 index 0000000000..29f7754896 --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_sprintfjs.js @@ -0,0 +1,120 @@ +/* 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 checks that our string formatter works with different patterns and + * arguments. + * Initially copied from unit tests at https://github.com/alexei/sprintf.js + */ + +const { sprintf } = require("resource://devtools/shared/sprintfjs/sprintf.js"); +const PI = 3.141592653589793; + +function run_test() { + // Simple patterns + equal("%", sprintf("%%")); + equal("10", sprintf("%b", 2)); + equal("A", sprintf("%c", 65)); + equal("2", sprintf("%d", 2)); + equal("2", sprintf("%i", 2)); + equal("2", sprintf("%d", "2")); + equal("2", sprintf("%i", "2")); + equal('{"foo":"bar"}', sprintf("%j", { foo: "bar" })); + equal('["foo","bar"]', sprintf("%j", ["foo", "bar"])); + equal("2e+0", sprintf("%e", 2)); + equal("2", sprintf("%u", 2)); + equal("4294967294", sprintf("%u", -2)); + equal("2.2", sprintf("%f", 2.2)); + equal("3.141592653589793", sprintf("%g", PI)); + equal("10", sprintf("%o", 8)); + equal("%s", sprintf("%s", "%s")); + equal("ff", sprintf("%x", 255)); + equal("FF", sprintf("%X", 255)); + equal( + "Polly wants a cracker", + sprintf("%2$s %3$s a %1$s", "cracker", "Polly", "wants") + ); + equal("Hello world!", sprintf("Hello %(who)s!", { who: "world" })); + equal("true", sprintf("%t", true)); + equal("t", sprintf("%.1t", true)); + equal("true", sprintf("%t", "true")); + equal("true", sprintf("%t", 1)); + equal("false", sprintf("%t", false)); + equal("f", sprintf("%.1t", false)); + equal("false", sprintf("%t", "")); + equal("false", sprintf("%t", 0)); + + equal("undefined", sprintf("%T", undefined)); + equal("null", sprintf("%T", null)); + equal("boolean", sprintf("%T", true)); + equal("number", sprintf("%T", 42)); + equal("string", sprintf("%T", "This is a string")); + equal("function", sprintf("%T", Math.log)); + equal("array", sprintf("%T", [1, 2, 3])); + equal("object", sprintf("%T", { foo: "bar" })); + + equal("regexp", sprintf("%T", /<("[^"]*"|"[^"]*"|[^"">])*>/)); + + equal("true", sprintf("%v", true)); + equal("42", sprintf("%v", 42)); + equal("This is a string", sprintf("%v", "This is a string")); + equal("1,2,3", sprintf("%v", [1, 2, 3])); + equal("[object Object]", sprintf("%v", { foo: "bar" })); + equal( + "/<(\"[^\"]*\"|'[^']*'|[^'\">])*>/", + sprintf("%v", /<("[^"]*"|'[^']*'|[^'">])*>/) + ); + + // sign + equal("2", sprintf("%d", 2)); + equal("-2", sprintf("%d", -2)); + equal("+2", sprintf("%+d", 2)); + equal("-2", sprintf("%+d", -2)); + equal("2", sprintf("%i", 2)); + equal("-2", sprintf("%i", -2)); + equal("+2", sprintf("%+i", 2)); + equal("-2", sprintf("%+i", -2)); + equal("2.2", sprintf("%f", 2.2)); + equal("-2.2", sprintf("%f", -2.2)); + equal("+2.2", sprintf("%+f", 2.2)); + equal("-2.2", sprintf("%+f", -2.2)); + equal("-2.3", sprintf("%+.1f", -2.34)); + equal("-0.0", sprintf("%+.1f", -0.01)); + equal("3.14159", sprintf("%.6g", PI)); + equal("3.14", sprintf("%.3g", PI)); + equal("3", sprintf("%.1g", PI)); + equal("-000000123", sprintf("%+010d", -123)); + equal("______-123", sprintf("%+'_10d", -123)); + equal("-234.34 123.2", sprintf("%f %f", -234.34, 123.2)); + + // padding + equal("-0002", sprintf("%05d", -2)); + equal("-0002", sprintf("%05i", -2)); + equal(" <", sprintf("%5s", "<")); + equal("0000<", sprintf("%05s", "<")); + equal("____<", sprintf("%'_5s", "<")); + equal("> ", sprintf("%-5s", ">")); + equal(">0000", sprintf("%0-5s", ">")); + equal(">____", sprintf("%'_-5s", ">")); + equal("xxxxxx", sprintf("%5s", "xxxxxx")); + equal("1234", sprintf("%02u", 1234)); + equal(" -10.235", sprintf("%8.3f", -10.23456)); + equal("-12.34 xxx", sprintf("%f %s", -12.34, "xxx")); + equal('{\n "foo": "bar"\n}', sprintf("%2j", { foo: "bar" })); + equal('[\n "foo",\n "bar"\n]', sprintf("%2j", ["foo", "bar"])); + + // precision + equal("2.3", sprintf("%.1f", 2.345)); + equal("xxxxx", sprintf("%5.5s", "xxxxxx")); + equal(" x", sprintf("%5.1s", "xxxxxx")); + + equal( + "foobar", + sprintf("%s", function () { + return "foobar"; + }) + ); +} diff --git a/devtools/shared/tests/xpcshell/test_stack.js b/devtools/shared/tests/xpcshell/test_stack.js new file mode 100644 index 0000000000..a95d28c57d --- /dev/null +++ b/devtools/shared/tests/xpcshell/test_stack.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test stack.js. + +function run_test() { + const loader = new DevToolsLoader(); + const require = loader.require; + + const { + StackFrameCache, + } = require("resource://devtools/server/actors/utils/stack.js"); + + const cache = new StackFrameCache(); + cache.initFrames(); + const baseFrame = { + line: 23, + column: 77, + source: "nowhere", + functionDisplayName: "nobody", + parent: null, + asyncParent: null, + asyncCause: null, + }; + cache.addFrame(baseFrame); + + let event = cache.makeEvent(); + Assert.equal(event[0], null); + Assert.equal(event[1].functionDisplayName, "nobody"); + Assert.equal(event.length, 2); + + cache.addFrame({ + line: 24, + column: 78, + source: "nowhere", + functionDisplayName: "still nobody", + parent: null, + asyncParent: baseFrame, + asyncCause: "async", + }); + + event = cache.makeEvent(); + Assert.equal(event[0].functionDisplayName, "still nobody"); + Assert.equal(event[0].parent, 0); + Assert.equal(event[0].asyncParent, 1); + Assert.equal(event.length, 1); +} diff --git a/devtools/shared/tests/xpcshell/throwing-module-1.js b/devtools/shared/tests/xpcshell/throwing-module-1.js new file mode 100644 index 0000000000..cc7e159a76 --- /dev/null +++ b/devtools/shared/tests/xpcshell/throwing-module-1.js @@ -0,0 +1,7 @@ +"use strict"; + +function throwingMethod() { + throw new Error("my-exception"); +} + +throwingMethod(); diff --git a/devtools/shared/tests/xpcshell/throwing-module-2.js b/devtools/shared/tests/xpcshell/throwing-module-2.js new file mode 100644 index 0000000000..3e723844ec --- /dev/null +++ b/devtools/shared/tests/xpcshell/throwing-module-2.js @@ -0,0 +1,8 @@ +"use strict"; + +function throwingMethod() { + // eslint-disable-next-line no-throw-literal + throw "my-exception"; +} + +throwingMethod(); diff --git a/devtools/shared/tests/xpcshell/xpcshell.toml b/devtools/shared/tests/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..360f42a480 --- /dev/null +++ b/devtools/shared/tests/xpcshell/xpcshell.toml @@ -0,0 +1,72 @@ +[DEFAULT] +tags = "devtools" +head = "head_devtools.js" +firefox-appdir = "browser" +skip-if = ["os == 'android'"] +support-files = [ + "exposeLoader.js", + "throwing-module-1.js", + "throwing-module-2.js", +] + +["test_assert.js"] + +["test_console_filtering.js"] + +["test_csslexer.js"] + +["test_debugger_client.js"] + +["test_defineLazyPrototypeGetter.js"] + +["test_eventemitter_abort_controller.js"] + +["test_eventemitter_basic.js"] + +["test_eventemitter_destroy.js"] + +["test_eventemitter_static.js"] + +["test_executeSoon.js"] + +["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_loader.js"] + +["test_natural-sort.js"] + +["test_pluralForm-english.js"] + +["test_pluralForm-makeGetter.js"] + +["test_prettifyCSS.js"] + +["test_require.js"] + +["test_require_lazy.js"] + +["test_require_raw.js"] + +["test_safeErrorString.js"] + +["test_sprintfjs.js"] + +["test_stack.js"] |