diff --git a/gfx/2d/BlurVMX.cpp b/gfx/2d/BlurVMX.cpp index 3ba12766c7..cf06ff45e0 100644 --- a/gfx/2d/BlurVMX.cpp +++ b/gfx/2d/BlurVMX.cpp @@ -295,10 +295,10 @@ void loop(int32_t startIdx, int32_t endIdx, vector float reciprocal, int32_t y) { - int topLeftIndex = reinterpret_cast(topLeftBase + startIdx) & 0xf ? 0 : 1; - int topRightIndex = reinterpret_cast(topRightBase + startIdx) & 0xf ? 0 : 1; - int bottomRightIndex = reinterpret_cast(bottomRightBase + startIdx) & 0xf ? 0 : 1; - int bottomLeftIndex = reinterpret_cast(bottomLeftBase + startIdx) & 0xf ? 0 : 1; + int topLeftIndex = static_cast(reinterpret_cast(topLeftBase + startIdx)) & 0xf ? 0 : 1; + int topRightIndex = static_cast(reinterpret_cast(topRightBase + startIdx)) & 0xf ? 0 : 1; + int bottomRightIndex = static_cast(reinterpret_cast(bottomRightBase + startIdx)) & 0xf ? 0 : 1; + int bottomLeftIndex = static_cast(reinterpret_cast(bottomLeftBase + startIdx)) & 0xf ? 0 : 1; vector unsigned char topLeftMask = vec_lvsl(0, reinterpret_cast(topLeftBase + startIdx)); vector unsigned int topLeftVector1 = vec_ld(0, topLeftBase + startIdx); diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index 2a8abe3f83..94838aa614 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -5075,6 +5075,20 @@ "n_buckets": "80 + 1", "description": "The time (in milliseconds) after showing a PopupNotification that the mainAction was first triggered" }, + "DEVTOOLS_DEBUGGER_RDP_LOCAL_ENUMPROPERTIES_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": "10000", + "n_buckets": "1000", + "description": "The time (in milliseconds) that it took a 'enumProperties' request to go round trip." + }, + "DEVTOOLS_DEBUGGER_RDP_REMOTE_ENUMPROPERTIES_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": "10000", + "n_buckets": "1000", + "description": "The time (in milliseconds) that it took a 'enumProperties' request to go round trip." + }, "VIDEO_CANPLAYTYPE_H264_CONSTRAINT_SET_FLAG": { "expires_in_version": "40", "kind": "enumerated", diff --git a/toolkit/devtools/DevToolsUtils.js b/toolkit/devtools/DevToolsUtils.js index 7b8aac0fbd..955437ea9a 100644 --- a/toolkit/devtools/DevToolsUtils.js +++ b/toolkit/devtools/DevToolsUtils.js @@ -49,7 +49,7 @@ exports.reportException = function reportException(aWho, aException) { dump(msg + "\n"); - if (Cu.reportError) { + if (Cu && Cu.reportError) { /* * Note that the xpcshell test harness registers an observer for * console messages, so when we're running tests, this will cause diff --git a/toolkit/devtools/Loader.jsm b/toolkit/devtools/Loader.jsm index f64be9cd46..664527d9fb 100644 --- a/toolkit/devtools/Loader.jsm +++ b/toolkit/devtools/Loader.jsm @@ -30,7 +30,6 @@ this.EXPORTED_SYMBOLS = ["DevToolsLoader", "devtools", "BuiltinProvider", let loaderModules = { "Services": Object.create(Services), "toolkit/loader": loader, - "promise": promise, "PromiseDebugging": PromiseDebugging }; XPCOMUtils.defineLazyGetter(loaderModules, "Debugger", () => { @@ -96,6 +95,7 @@ BuiltinProvider.prototype = { "devtools/content-observer": "resource://gre/modules/devtools/content-observer", "gcli": "resource://gre/modules/devtools/gcli", "projecteditor": "resource://gre/modules/devtools/projecteditor", + "promise": "resource://gre/modules/Promise-backend.js", "acorn": "resource://gre/modules/devtools/acorn", "acorn/util/walk": "resource://gre/modules/devtools/acorn/walk.js", "tern": "resource://gre/modules/devtools/tern", @@ -135,6 +135,7 @@ SrcdirProvider.prototype = { srcdir = OS.Path.normalize(srcdir.data.trim()); let devtoolsDir = OS.Path.join(srcdir, "browser", "devtools"); let toolkitDir = OS.Path.join(srcdir, "toolkit", "devtools"); + let modulesDir = OS.Path.join(srcdir, "toolkit", "modules"); let mainURI = this.fileURI(OS.Path.join(devtoolsDir, "main.js")); let definitionsURI = this.fileURI(OS.Path.join(devtoolsDir, "definitions.js")); let devtoolsURI = this.fileURI(devtoolsDir); @@ -153,6 +154,7 @@ SrcdirProvider.prototype = { let contentObserverURI = this.fileURI(OS.Path.join(toolkitDir), "content-observer.js"); let gcliURI = this.fileURI(OS.Path.join(toolkitDir, "gcli", "source", "lib", "gcli")); let projecteditorURI = this.fileURI(OS.Path.join(devtoolsDir, "projecteditor")); + let promiseURI = this.fileURI(OS.Path.join(modulesDir, "promise-backend.js")); let acornURI = this.fileURI(OS.Path.join(toolkitDir, "acorn")); let acornWalkURI = OS.Path.join(acornURI, "walk.js"); let ternURI = OS.Path.join(toolkitDir, "tern"); @@ -180,6 +182,7 @@ SrcdirProvider.prototype = { "devtools/content-observer": contentObserverURI, "gcli": gcliURI, "projecteditor": projecteditorURI, + "promise": promiseURI, "acorn": acornURI, "acorn/util/walk": acornWalkURI, "tern": ternURI, diff --git a/toolkit/devtools/client/dbg-client.jsm b/toolkit/devtools/client/dbg-client.jsm index 2c490a79d4..ec0e40567b 100644 --- a/toolkit/devtools/client/dbg-client.jsm +++ b/toolkit/devtools/client/dbg-client.jsm @@ -2171,7 +2171,36 @@ ObjectClient.prototype = { } return aPacket; } - }) + }), + + /** + * Request the promises directly depending on the current promise. + */ + getDependentPromises: DebuggerClient.requester({ + type: "dependentPromises" + }, { + before: function(aPacket) { + if (this._grip.class !== "Promise") { + throw new Error("getDependentPromises is only valid for promise " + + "grips."); + } + return aPacket; + } + }), + + /** + * Request the stack to the promise's allocation point. + */ + getPromiseAllocationStack: DebuggerClient.requester({ + type: "allocationStack" + }, { + before: function(aPacket) { + if (this._grip.class !== "Promise") { + throw new Error("getAllocationStack is only valid for promise grips."); + } + return aPacket; + } + }), }; /** @@ -2320,6 +2349,39 @@ SourceClient.prototype = { }); }, + /** + * Request a PropertyIteratorClient instance to ease listing + * properties for this object. + * + * @param options Object + * A dictionary object with various boolean attributes: + * - ignoreSafeGetters Boolean + * If true, do not iterate over safe getters. + * - ignoreIndexedProperties Boolean + * If true, filters out Array items. + * e.g. properties names between `0` and `object.length`. + * - ignoreNonIndexedProperties Boolean + * If true, filters out items that aren't array items + * e.g. properties names that are not a number between `0` + * and `object.length`. + * - sort Boolean + * If true, the iterator will sort the properties by name + * before dispatching them. + * @param aOnResponse function Called with the client instance. + */ + enumProperties: DebuggerClient.requester({ + type: "enumProperties", + options: args(0) + }, { + after: function(aResponse) { + if (aResponse.iterator) { + return { iterator: new PropertyIteratorClient(this._client, aResponse.iterator) }; + } + return aResponse; + }, + telemetry: "ENUMPROPERTIES" + }), + /** * Pretty print this source's text. */ @@ -2458,6 +2520,74 @@ SourceClient.prototype = { } }; +/** + * A PropertyIteratorClient provides a way to access to property names and + * values of an object efficiently, slice by slice. + * Note that the properties can be sorted in the backend, + * this is controled while creating the PropertyIteratorClient + * from ObjectClient.enumProperties. + * + * @param aClient DebuggerClient + * The debugger client parent. + * @param aGrip Object + * A PropertyIteratorActor grip returned by the protocol via + * TabActor.enumProperties request. + */ +function PropertyIteratorClient(aClient, aGrip) { + this._grip = aGrip; + this._client = aClient; + this.request = this._client.request; +} + +PropertyIteratorClient.prototype = { + get actor() { return this._grip.actor; }, + + /** + * Get the total number of properties available in the iterator. + */ + get count() { return this._grip.count; }, + + /** + * Get one or more property names that correspond to the positions in the + * indexes parameter. + * + * @param indexes Array + * An array of property indexes. + * @param aCallback Function + * The function called when we receive the property names. + */ + names: DebuggerClient.requester({ + type: "names", + indexes: args(0) + }, {}), + + /** + * Get a set of following property value(s). + * + * @param start Number + * The index of the first property to fetch. + * @param count Number + * The number of properties to fetch. + * @param aCallback Function + * The function called when we receive the property values. + */ + slice: DebuggerClient.requester({ + type: "slice", + start: args(0), + count: args(1) + }, {}), + + /** + * Get all the property values. + * + * @param aCallback Function + * The function called when we receive the property values. + */ + all: DebuggerClient.requester({ + type: "all" + }, {}), +}; + /** * Breakpoint clients are used to remove breakpoints that are no longer used. * diff --git a/toolkit/devtools/debugger/debugger-view.js b/toolkit/devtools/debugger/debugger-view.js index 11c3d798f8..9c9f867982 100644 --- a/toolkit/devtools/debugger/debugger-view.js +++ b/toolkit/devtools/debugger/debugger-view.js @@ -424,7 +424,8 @@ let DebuggerView = { deferred.resolve([aSource, aText, aContentType]); }, ([, aError]) => { - let msg = L10N.getStr("errorLoadingText") + DevToolsUtils.safeErrorString(aError); + let url = aError; + let msg = L10N.getFormatStr("errorLoadingText2", url); this._setEditorText(msg); Cu.reportError(msg); dumpn(msg); diff --git a/toolkit/devtools/debugger/test/browser.ini b/toolkit/devtools/debugger/test/browser.ini index 97b30a66c3..2fe07751c4 100644 --- a/toolkit/devtools/debugger/test/browser.ini +++ b/toolkit/devtools/debugger/test/browser.ini @@ -78,6 +78,7 @@ support-files = doc_pretty-print-2.html doc_pretty-print-3.html doc_pretty-print-on-paused.html + doc_promise-get-allocation-stack.html doc_promise.html doc_random-javascript.html doc_recursion-stack.html @@ -330,7 +331,11 @@ skip-if = e10s && debug [browser_dbg_pretty-print-on-paused.js] skip-if = e10s && debug [browser_dbg_progress-listener-bug.js] -skip-if = e10a && debug +skip-if = e10s && debug +[browser_dbg_promises-allocation-stack.js] +skip-if = e10s && debug +[browser_dbg_promises-chrome-allocation-stack.js] +skip-if = e10s && debug [browser_dbg_reload-preferred-script-01.js] skip-if = e10s && debug [browser_dbg_reload-preferred-script-02.js] diff --git a/toolkit/devtools/debugger/test/browser_dbg_promises-allocation-stack.js b/toolkit/devtools/debugger/test/browser_dbg_promises-allocation-stack.js new file mode 100644 index 0000000000..5d0f6fd9b5 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_promises-allocation-stack.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we can get a stack to a promise's allocation point. + */ + +"use strict"; + +const TAB_URL = EXAMPLE_URL + "doc_promise-get-allocation-stack.html"; +const { PromisesFront } = devtools.require("devtools/server/actors/promises"); +let events = devtools.require("sdk/event/core"); + +function test() { + Task.spawn(function* () { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + + const [ tab,, panel ] = yield initDebugger(TAB_URL); + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + yield connect(client); + + let { tabs } = yield listTabs(client); + let targetTab = findTab(tabs, TAB_URL); + yield attachTab(client, targetTab); + + yield testGetAllocationStack(client, targetTab, tab); + + yield close(client); + yield closeDebuggerAndFinish(panel); + }).then(null, error => { + ok(false, "Got an error: " + error.message + "\n" + error.stack); + }); +} + +function* testGetAllocationStack(client, form, tab) { + let front = PromisesFront(client, form); + + yield front.attach(); + yield front.listPromises(); + + // Get the grip for promise p + let onNewPromise = new Promise(resolve => { + events.on(front, "new-promises", promises => { + for (let p of promises) { + if (p.preview.ownProperties.name && + p.preview.ownProperties.name.value === "p") { + resolve(p); + } + } + }); + }); + + callInTab(tab, "makePromises"); + + let grip = yield onNewPromise; + ok(grip, "Found our promise p"); + + let objectClient = new ObjectClient(client, grip); + ok(objectClient, "Got Object Client"); + + yield new Promise(resolve => { + objectClient.getPromiseAllocationStack(response => { + ok(response.allocationStack.length, "Got promise allocation stack."); + + for (let stack of response.allocationStack) { + is(stack.source.url, TAB_URL, "Got correct source URL."); + is(stack.functionDisplayName, "makePromises", + "Got correct function display name."); + is(typeof stack.line, "number", "Expect stack line to be a number."); + is(typeof stack.column, "number", + "Expect stack column to be a number."); + } + + resolve(); + }); + }); + + yield front.detach(); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_promises-chrome-allocation-stack.js b/toolkit/devtools/debugger/test/browser_dbg_promises-chrome-allocation-stack.js new file mode 100644 index 0000000000..a15528f712 --- /dev/null +++ b/toolkit/devtools/debugger/test/browser_dbg_promises-chrome-allocation-stack.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we can get a stack to a promise's allocation point in the chrome + * process. + */ + +"use strict"; + +const SOURCE_URL = "browser_dbg_promises-chrome-allocation-stack.js"; +const { PromisesFront } = devtools.require("devtools/server/actors/promises"); +let events = devtools.require("sdk/event/core"); + +const STACK_DATA = [ + { functionDisplayName: "test/ { + let p = new Promise(() => {}); + p.name = "p"; + let q = p.then(); + q.name = "q"; + let r = p.then(null, () => {}); + r.name = "r"; + }); + + yield close(client); + finish(); + }).then(null, error => { + ok(false, "Got an error: " + error.message + "\n" + error.stack); + }); +} + +function* testGetAllocationStack(client, form, makePromises) { + let front = PromisesFront(client, form); + + yield front.attach(); + yield front.listPromises(); + + // Get the grip for promise p + let onNewPromise = new Promise(resolve => { + events.on(front, "new-promises", promises => { + for (let p of promises) { + if (p.preview.ownProperties.name && + p.preview.ownProperties.name.value === "p") { + resolve(p); + } + } + }); + }); + + makePromises(); + + let grip = yield onNewPromise; + ok(grip, "Found our promise p"); + + let objectClient = new ObjectClient(client, grip); + ok(objectClient, "Got Object Client"); + + yield new Promise(resolve => { + objectClient.getPromiseAllocationStack(response => { + ok(response.allocationStack.length, "Got promise allocation stack."); + + for (let i = 0; i < STACK_DATA.length; i++) { + let data = STACK_DATA[i]; + let stack = response.allocationStack[i]; + + ok(stack.source.url.startsWith("chrome:"), "Got a chrome source URL"); + ok(stack.source.url.endsWith(SOURCE_URL), "Got correct source URL."); + is(stack.functionDisplayName, data.functionDisplayName, + "Got correct function display name."); + is(typeof stack.line, "number", "Expect stack line to be a number."); + is(typeof stack.column, "number", + "Expect stack column to be a number."); + } + + resolve(); + }); + }); + + yield front.detach(); +} diff --git a/toolkit/devtools/debugger/test/browser_dbg_scripts-switching-03.js b/toolkit/devtools/debugger/test/browser_dbg_scripts-switching-03.js index ddc34428aa..9da115b1a8 100644 --- a/toolkit/devtools/debugger/test/browser_dbg_scripts-switching-03.js +++ b/toolkit/devtools/debugger/test/browser_dbg_scripts-switching-03.js @@ -36,7 +36,7 @@ function showBogusSource() { } function testDebuggerLoadingError() { - ok(gEditor.getText().contains(gL10N.getStr("errorLoadingText")), + ok(gEditor.getText().includes(gL10N.getFormatStr("errorLoadingText2", "noSuchActor")), "The valid error loading message is displayed."); } diff --git a/toolkit/devtools/debugger/test/browser_dbg_variables-view-large-array-buffer.js b/toolkit/devtools/debugger/test/browser_dbg_variables-view-large-array-buffer.js index d543b49a45..0542b3d369 100644 --- a/toolkit/devtools/debugger/test/browser_dbg_variables-view-large-array-buffer.js +++ b/toolkit/devtools/debugger/test/browser_dbg_variables-view-large-array-buffer.js @@ -58,7 +58,7 @@ function initialChecks() { is(objectVar.target.querySelector(".name").getAttribute("value"), "largeObject", "Should have the right property name for 'largeObject'."); - is(objectVar.target.querySelector(".value").getAttribute("value"), "Object", + is(objectVar.target.querySelector(".value").getAttribute("value"), "Object[10000]", "Should have the right property value for 'largeObject'."); ok(objectVar.target.querySelector(".value").className.contains("token-other"), "Should have the right token class for 'largeObject'."); @@ -70,10 +70,7 @@ function initialChecks() { is(objectVar.expanded, false, "The 'largeObject' variable shouldn't be expanded."); - let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 2); - arrayVar.expand(); - objectVar.expand(); - return finished; + return promise.all([arrayVar.expand(),objectVar.expand()]); } function verifyFirstLevel() { @@ -96,55 +93,56 @@ function verifyFirstLevel() { "The 'largeObject' should contain all the created non-enumerable elements."); is(arrayVar.target.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"), - 0 + gEllipsis + 1999, "The first page in the 'largeArray' is named correctly."); + "[0" + gEllipsis + "2499]", "The first page in the 'largeArray' is named correctly."); is(arrayVar.target.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"), "", "The first page in the 'largeArray' should not have a corresponding value."); is(arrayVar.target.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"), - 2000 + gEllipsis + 3999, "The second page in the 'largeArray' is named correctly."); + "[2500" + gEllipsis + "4999]", "The second page in the 'largeArray' is named correctly."); is(arrayVar.target.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"), "", "The second page in the 'largeArray' should not have a corresponding value."); is(arrayVar.target.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"), - 4000 + gEllipsis + 5999, "The third page in the 'largeArray' is named correctly."); + "[5000" + gEllipsis + "7499]", "The third page in the 'largeArray' is named correctly."); is(arrayVar.target.querySelectorAll(".variables-view-property .value")[2].getAttribute("value"), "", "The third page in the 'largeArray' should not have a corresponding value."); is(arrayVar.target.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"), - 6000 + gEllipsis + 9999, "The fourth page in the 'largeArray' is named correctly."); + "[7500" + gEllipsis + "9999]", "The fourth page in the 'largeArray' is named correctly."); is(arrayVar.target.querySelectorAll(".variables-view-property .value")[3].getAttribute("value"), "", "The fourth page in the 'largeArray' should not have a corresponding value."); is(objectVar.target.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"), - 0 + gEllipsis + 1999, "The first page in the 'largeObject' is named correctly."); + "[0" + gEllipsis + "2499]", "The first page in the 'largeObject' is named correctly."); is(objectVar.target.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"), "", "The first page in the 'largeObject' should not have a corresponding value."); is(objectVar.target.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"), - 2000 + gEllipsis + 3999, "The second page in the 'largeObject' is named correctly."); + "[2500" + gEllipsis + "4999]", "The second page in the 'largeObject' is named correctly."); is(objectVar.target.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"), "", "The second page in the 'largeObject' should not have a corresponding value."); is(objectVar.target.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"), - 4000 + gEllipsis + 5999, "The thrid page in the 'largeObject' is named correctly."); + "[5000" + gEllipsis + "7499]", "The thrid page in the 'largeObject' is named correctly."); is(objectVar.target.querySelectorAll(".variables-view-property .value")[2].getAttribute("value"), "", "The thrid page in the 'largeObject' should not have a corresponding value."); is(objectVar.target.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"), - 6000 + gEllipsis + 9999, "The fourth page in the 'largeObject' is named correctly."); + "[7500" + gEllipsis + "9999]", "The fourth page in the 'largeObject' is named correctly."); is(objectVar.target.querySelectorAll(".variables-view-property .value")[3].getAttribute("value"), "", "The fourth page in the 'largeObject' should not have a corresponding value."); is(arrayVar.target.querySelectorAll(".variables-view-property .name")[4].getAttribute("value"), - "length", "The other properties 'largeArray' are named correctly."); - is(arrayVar.target.querySelectorAll(".variables-view-property .value")[4].getAttribute("value"), - "10000", "The other properties 'largeArray' have the correct value."); - is(arrayVar.target.querySelectorAll(".variables-view-property .name")[5].getAttribute("value"), "buffer", "The other properties 'largeArray' are named correctly."); - is(arrayVar.target.querySelectorAll(".variables-view-property .value")[5].getAttribute("value"), + is(arrayVar.target.querySelectorAll(".variables-view-property .value")[4].getAttribute("value"), "ArrayBuffer", "The other properties 'largeArray' have the correct value."); - is(arrayVar.target.querySelectorAll(".variables-view-property .name")[6].getAttribute("value"), + is(arrayVar.target.querySelectorAll(".variables-view-property .name")[5].getAttribute("value"), "byteLength", "The other properties 'largeArray' are named correctly."); - is(arrayVar.target.querySelectorAll(".variables-view-property .value")[6].getAttribute("value"), + is(arrayVar.target.querySelectorAll(".variables-view-property .value")[5].getAttribute("value"), "10000", "The other properties 'largeArray' have the correct value."); - is(arrayVar.target.querySelectorAll(".variables-view-property .name")[7].getAttribute("value"), + is(arrayVar.target.querySelectorAll(".variables-view-property .name")[6].getAttribute("value"), "byteOffset", "The other properties 'largeArray' are named correctly."); - is(arrayVar.target.querySelectorAll(".variables-view-property .value")[7].getAttribute("value"), + is(arrayVar.target.querySelectorAll(".variables-view-property .value")[6].getAttribute("value"), "0", "The other properties 'largeArray' have the correct value."); + is(arrayVar.target.querySelectorAll(".variables-view-property .name")[7].getAttribute("value"), + "length", "The other properties 'largeArray' are named correctly."); + is(arrayVar.target.querySelectorAll(".variables-view-property .value")[7].getAttribute("value"), + "10000", "The other properties 'largeArray' have the correct value."); + is(arrayVar.target.querySelectorAll(".variables-view-property .name")[8].getAttribute("value"), "__proto__", "The other properties 'largeArray' are named correctly."); is(arrayVar.target.querySelectorAll(".variables-view-property .value")[8].getAttribute("value"), @@ -160,10 +158,13 @@ function verifyNextLevels() { let localScope = gVariables.getScopeAtIndex(0); let objectVar = localScope.get("largeObject"); - let lastPage1 = objectVar.get(6000 + gEllipsis + 9999); + let lastPage1 = objectVar.get("[7500" + gEllipsis + "9999]"); ok(lastPage1, "The last page in the first level was retrieved successfully."); - lastPage1.expand(); + return lastPage1.expand() + .then(verifyNextLevels2.bind(null, lastPage1)); +} +function verifyNextLevels2(lastPage1) { let pageEnums1 = lastPage1.target.querySelector(".variables-view-element-details.enum").childNodes; let pageNonEnums1 = lastPage1.target.querySelector(".variables-view-element-details.nonenum").childNodes; is(pageEnums1.length, 0, @@ -172,61 +173,44 @@ function verifyNextLevels() { "The last page in the first level should contain all the created non-enumerable elements."); is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"), - 6000 + gEllipsis + 6999, "The first page in this level named correctly (1)."); + "[7500" + gEllipsis + "8124]", "The first page in this level named correctly (1)."); is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"), - 7000 + gEllipsis + 7999, "The second page in this level named correctly (1)."); + "[8125" + gEllipsis + "8749]", "The second page in this level named correctly (1)."); is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"), - 8000 + gEllipsis + 8999, "The third page in this level named correctly (1)."); + "[8750" + gEllipsis + "9374]", "The third page in this level named correctly (1)."); is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"), - 9000 + gEllipsis + 9999, "The fourth page in this level named correctly (1)."); + "[9375" + gEllipsis + "9999]", "The fourth page in this level named correctly (1)."); - let lastPage2 = lastPage1.get(9000 + gEllipsis + 9999); + let lastPage2 = lastPage1.get("[9375" + gEllipsis + "9999]"); ok(lastPage2, "The last page in the second level was retrieved successfully."); - lastPage2.expand(); + return lastPage2.expand() + .then(verifyNextLevels3.bind(null, lastPage2)); +} +function verifyNextLevels3(lastPage2) { let pageEnums2 = lastPage2.target.querySelector(".variables-view-element-details.enum").childNodes; let pageNonEnums2 = lastPage2.target.querySelector(".variables-view-element-details.nonenum").childNodes; - is(pageEnums2.length, 0, - "The last page in the second level shouldn't contain any enumerable elements."); - is(pageNonEnums2.length, 4, - "The last page in the second level should contain all the created non-enumerable elements."); - - is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"), - 9000 + gEllipsis + 9199, "The first page in this level named correctly (2)."); - is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"), - 9200 + gEllipsis + 9399, "The second page in this level named correctly (2)."); - is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"), - 9400 + gEllipsis + 9599, "The third page in this level named correctly (2)."); - is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"), - 9600 + gEllipsis + 9999, "The fourth page in this level named correctly (2)."); - - let lastPage3 = lastPage2.get(9600 + gEllipsis + 9999); - ok(lastPage3, "The last page in the third level was retrieved successfully."); - lastPage3.expand(); - - let pageEnums3 = lastPage3.target.querySelector(".variables-view-element-details.enum").childNodes; - let pageNonEnums3 = lastPage3.target.querySelector(".variables-view-element-details.nonenum").childNodes; - is(pageEnums3.length, 400, + is(pageEnums2.length, 625, "The last page in the third level should contain all the created enumerable elements."); - is(pageNonEnums3.length, 0, + is(pageNonEnums2.length, 0, "The last page in the third level shouldn't contain any non-enumerable elements."); - is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"), - 9600, "The properties in this level are named correctly (3)."); - is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"), - 9601, "The properties in this level are named correctly (3)."); - is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[398].getAttribute("value"), + is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"), + 9375, "The properties in this level are named correctly (3)."); + is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"), + 9376, "The properties in this level are named correctly (3)."); + is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[623].getAttribute("value"), 9998, "The properties in this level are named correctly (3)."); - is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[399].getAttribute("value"), + is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[624].getAttribute("value"), 9999, "The properties in this level are named correctly (3)."); - is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"), - 399, "The properties in this level have the correct value (3)."); - is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"), - 398, "The properties in this level have the correct value (3)."); - is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[398].getAttribute("value"), + is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"), + 624, "The properties in this level have the correct value (3)."); + is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"), + 623, "The properties in this level have the correct value (3)."); + is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[623].getAttribute("value"), 1, "The properties in this level have the correct value (3)."); - is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[399].getAttribute("value"), + is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[624].getAttribute("value"), 0, "The properties in this level have the correct value (3)."); } diff --git a/toolkit/devtools/debugger/test/doc_promise-get-allocation-stack.html b/toolkit/devtools/debugger/test/doc_promise-get-allocation-stack.html new file mode 100644 index 0000000000..48546c9671 --- /dev/null +++ b/toolkit/devtools/debugger/test/doc_promise-get-allocation-stack.html @@ -0,0 +1,24 @@ + + + + + + + Promise test page + + + + + + + diff --git a/toolkit/devtools/debugger/test/head.js b/toolkit/devtools/debugger/test/head.js index 511dffc99c..7768cd5cd5 100644 --- a/toolkit/devtools/debugger/test/head.js +++ b/toolkit/devtools/debugger/test/head.js @@ -20,7 +20,8 @@ let { require } = devtools; let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {}); let { BrowserToolboxProcess } = Cu.import("resource://gre/modules/devtools/ToolboxProcess.jsm", {}); let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {}); -let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {}); +let { DebuggerClient, ObjectClient } = + Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {}); let { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {}); let EventEmitter = require("devtools/toolkit/event-emitter"); const { promiseInvoke } = require("devtools/async-utils"); diff --git a/toolkit/devtools/server/actors/addon.js b/toolkit/devtools/server/actors/addon.js new file mode 100644 index 0000000000..b7a96ede0f --- /dev/null +++ b/toolkit/devtools/server/actors/addon.js @@ -0,0 +1,335 @@ +/* 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"; + +let { Ci, Cu } = require("chrome"); +let Services = require("Services"); +let { ActorPool } = require("devtools/server/actors/common"); +let { TabSources } = require("./utils/TabSources"); +let makeDebugger = require("./utils/make-debugger"); +let { ConsoleAPIListener } = require("devtools/toolkit/webconsole/utils"); +let DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); +let { dbg_assert, update } = DevToolsUtils; + +loader.lazyRequireGetter(this, "AddonThreadActor", "devtools/server/actors/script", true); +loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true); +loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id"); +loader.lazyRequireGetter(this, "WebConsoleActor", "devtools/server/actors/webconsole", true); + +loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); + +function BrowserAddonActor(aConnection, aAddon) { + this.conn = aConnection; + this._addon = aAddon; + this._contextPool = new ActorPool(this.conn); + this.conn.addActorPool(this._contextPool); + this.threadActor = null; + this._global = null; + + this._shouldAddNewGlobalAsDebuggee = this._shouldAddNewGlobalAsDebuggee.bind(this); + + this.makeDebugger = makeDebugger.bind(null, { + findDebuggees: this._findDebuggees.bind(this), + shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee + }); + + AddonManager.addAddonListener(this); +} +exports.BrowserAddonActor = BrowserAddonActor; + +BrowserAddonActor.prototype = { + actorPrefix: "addon", + + get exited() { + return !this._addon; + }, + + get id() { + return this._addon.id; + }, + + get url() { + return this._addon.sourceURI ? this._addon.sourceURI.spec : undefined; + }, + + get attached() { + return this.threadActor; + }, + + get global() { + return this._global; + }, + + get sources() { + if (!this._sources) { + dbg_assert(this.threadActor, "threadActor should exist when creating sources."); + this._sources = new TabSources(this.threadActor, this._allowSource); + } + return this._sources; + }, + + + form: function BAA_form() { + dbg_assert(this.actorID, "addon should have an actorID."); + if (!this._consoleActor) { + this._consoleActor = new AddonConsoleActor(this._addon, this.conn, this); + this._contextPool.addActor(this._consoleActor); + } + + return { + actor: this.actorID, + id: this.id, + name: this._addon.name, + url: this.url, + debuggable: this._addon.isDebuggable, + consoleActor: this._consoleActor.actorID, + + traits: { + highlightable: false, + networkMonitor: false, + }, + }; + }, + + disconnect: function BAA_disconnect() { + this.conn.removeActorPool(this._contextPool); + this._contextPool = null; + this._consoleActor = null; + this._addon = null; + this._global = null; + AddonManager.removeAddonListener(this); + }, + + setOptions: function BAA_setOptions(aOptions) { + if ("global" in aOptions) { + this._global = aOptions.global; + } + }, + + onDisabled: function BAA_onDisabled(aAddon) { + if (aAddon != this._addon) { + return; + } + + this._global = null; + }, + + onUninstalled: function BAA_onUninstalled(aAddon) { + if (aAddon != this._addon) { + return; + } + + if (this.attached) { + this.onDetach(); + this.conn.send({ from: this.actorID, type: "tabDetached" }); + } + + this.disconnect(); + }, + + onAttach: function BAA_onAttach() { + if (this.exited) { + return { type: "exited" }; + } + + if (!this.attached) { + this.threadActor = new AddonThreadActor(this.conn, this); + this._contextPool.addActor(this.threadActor); + } + + return { type: "tabAttached", threadActor: this.threadActor.actorID }; + }, + + onDetach: function BAA_onDetach() { + if (!this.attached) { + return { error: "wrongState" }; + } + + this._contextPool.removeActor(this.threadActor); + + this.threadActor = null; + this._sources = null; + + return { type: "detached" }; + }, + + preNest: function() { + let e = Services.wm.getEnumerator(null); + while (e.hasMoreElements()) { + let win = e.getNext(); + let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + windowUtils.suppressEventHandling(true); + windowUtils.suspendTimeouts(); + } + }, + + postNest: function() { + let e = Services.wm.getEnumerator(null); + while (e.hasMoreElements()) { + let win = e.getNext(); + let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + windowUtils.resumeTimeouts(); + windowUtils.suppressEventHandling(false); + } + }, + + /** + * Return true if the given global is associated with this addon and should be + * added as a debuggee, false otherwise. + */ + _shouldAddNewGlobalAsDebuggee: function (aGlobal) { + const global = unwrapDebuggerObjectGlobal(aGlobal); + try { + // This will fail for non-Sandbox objects, hence the try-catch block. + let metadata = Cu.getSandboxMetadata(global); + if (metadata) { + return metadata.addonID === this.id; + } + } catch (e) {} + + if (global instanceof Ci.nsIDOMWindow) { + let id = {}; + if (mapURIToAddonID(global.document.documentURIObject, id)) { + return id.value === this.id; + } + return false; + } + + // Check the global for a __URI__ property and then try to map that to an + // add-on + let uridescriptor = aGlobal.getOwnPropertyDescriptor("__URI__"); + if (uridescriptor && "value" in uridescriptor && uridescriptor.value) { + let uri; + try { + uri = Services.io.newURI(uridescriptor.value, null, null); + } + catch (e) { + DevToolsUtils.reportException( + "BrowserAddonActor.prototype._shouldAddNewGlobalAsDebuggee", + new Error("Invalid URI: " + uridescriptor.value) + ); + return false; + } + + let id = {}; + if (mapURIToAddonID(uri, id)) { + return id.value === this.id; + } + } + + return false; + }, + + /** + * Override the eligibility check for scripts and sources to make + * sure every script and source with a URL is stored when debugging + * add-ons. + */ + _allowSource: function(aSource) { + // XPIProvider.jsm evals some code in every add-on's bootstrap.js. Hide it. + if (aSource.url === "resource://gre/modules/addons/XPIProvider.jsm") { + return false; + } + + return true; + }, + + /** + * Yield the current set of globals associated with this addon that should be + * added as debuggees. + */ + _findDebuggees: function (dbg) { + return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee); + } +}; + +BrowserAddonActor.prototype.requestTypes = { + "attach": BrowserAddonActor.prototype.onAttach, + "detach": BrowserAddonActor.prototype.onDetach +}; + +/** + * The AddonConsoleActor implements capabilities needed for the add-on web + * console feature. + * + * @constructor + * @param object aAddon + * The add-on that this console watches. + * @param object aConnection + * The connection to the client, DebuggerServerConnection. + * @param object aParentActor + * The parent BrowserAddonActor actor. + */ +function AddonConsoleActor(aAddon, aConnection, aParentActor) +{ + this.addon = aAddon; + WebConsoleActor.call(this, aConnection, aParentActor); +} + +AddonConsoleActor.prototype = Object.create(WebConsoleActor.prototype); + +update(AddonConsoleActor.prototype, { + constructor: AddonConsoleActor, + + actorPrefix: "addonConsole", + + /** + * The add-on that this console watches. + */ + addon: null, + + /** + * The main add-on JS global + */ + get window() { + return this.parentActor.global; + }, + + /** + * Destroy the current AddonConsoleActor instance. + */ + disconnect: function ACA_disconnect() + { + WebConsoleActor.prototype.disconnect.call(this); + this.addon = null; + }, + + /** + * Handler for the "startListeners" request. + * + * @param object aRequest + * The JSON request object received from the Web Console client. + * @return object + * The response object which holds the startedListeners array. + */ + onStartListeners: function ACA_onStartListeners(aRequest) + { + let startedListeners = []; + + while (aRequest.listeners.length > 0) { + let listener = aRequest.listeners.shift(); + switch (listener) { + case "ConsoleAPI": + if (!this.consoleAPIListener) { + this.consoleAPIListener = + new ConsoleAPIListener(null, this, "addon/" + this.addon.id); + this.consoleAPIListener.init(); + } + startedListeners.push(listener); + break; + } + } + return { + startedListeners: startedListeners, + nativeConsoleAPI: true, + traits: this.traits, + }; + }, +}); + +AddonConsoleActor.prototype.requestTypes = Object.create(WebConsoleActor.prototype.requestTypes); +AddonConsoleActor.prototype.requestTypes.startListeners = AddonConsoleActor.prototype.onStartListeners; diff --git a/toolkit/devtools/server/actors/child-process.js b/toolkit/devtools/server/actors/child-process.js index b6673749e1..c87ad738b4 100644 --- a/toolkit/devtools/server/actors/child-process.js +++ b/toolkit/devtools/server/actors/child-process.js @@ -11,12 +11,14 @@ const { WebConsoleActor } = require("devtools/server/actors/webconsole"); const makeDebugger = require("devtools/server/actors/utils/make-debugger"); const { ActorPool } = require("devtools/server/main"); const Services = require("Services"); +const { dbg_assert } = require("devtools/toolkit/DevToolsUtils"); +const { TabSources } = require("./utils/TabSources"); function ChildProcessActor(aConnection) { this.conn = aConnection; this._contextPool = new ActorPool(this.conn); this.conn.addActorPool(this._contextPool); - this._threadActor = null; + this.threadActor = null; // Use a see-everything debugger this.makeDebugger = makeDebugger.bind(null, { @@ -52,15 +54,23 @@ ChildProcessActor.prototype = { return this._consoleScope; }, + get sources() { + if (!this._sources) { + dbg_assert(this.threadActor, "threadActor should exist when creating sources."); + this._sources = new TabSources(this.threadActor); + } + return this._sources; + }, + form: function() { if (!this._consoleActor) { this._consoleActor = new WebConsoleActor(this.conn, this); this._contextPool.addActor(this._consoleActor); } - if (!this._threadActor) { - this._threadActor = new ChromeDebuggerActor(this.conn, this); - this._contextPool.addActor(this._threadActor); + if (!this.threadActor) { + this.threadActor = new ChromeDebuggerActor(this.conn, this); + this._contextPool.addActor(this.threadActor); } return { @@ -68,7 +78,7 @@ ChildProcessActor.prototype = { name: "Content process", consoleActor: this._consoleActor.actorID, - chromeDebugger: this._threadActor.actorID, + chromeDebugger: this.threadActor.actorID, traits: { highlightable: false, diff --git a/toolkit/devtools/server/actors/object.js b/toolkit/devtools/server/actors/object.js new file mode 100644 index 0000000000..32eb81dcb5 --- /dev/null +++ b/toolkit/devtools/server/actors/object.js @@ -0,0 +1,1900 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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 { Cu, Ci } = require("chrome"); +const { GeneratedLocation } = require("devtools/server/actors/common"); +const { DebuggerServer } = require("devtools/server/main") +const DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); +const { dbg_assert } = DevToolsUtils; +const PromiseDebugging = require("PromiseDebugging"); + +const TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array", + "Uint32Array", "Int8Array", "Int16Array", "Int32Array", "Float32Array", + "Float64Array"]; + +// Number of items to preview in objects, arrays, maps, sets, lists, +// collections, etc. +const OBJECT_PREVIEW_MAX_ITEMS = 10; + +/** + * Creates an actor for the specified object. + * + * @param obj Debugger.Object + * The debuggee object. + * @param hooks Object + * A collection of abstract methods that are implemented by the caller. + * ObjectActor requires the following functions to be implemented by + * the caller: + * - createValueGrip + * Creates a value grip for the given object + * - sources + * TabSources getter that manages the sources of a thread + * - createEnvironmentActor + * Creates and return an environment actor + * - getGripDepth + * An actor's grip depth getter + * - incrementGripDepth + * Increment the actor's grip depth + * - decrementGripDepth + * Decrement the actor's grip depth + * - globalDebugObject + * The Debuggee Global Object as given by the ThreadActor + */ +function ObjectActor(obj, { + createValueGrip, + sources, + createEnvironmentActor, + getGripDepth, + incrementGripDepth, + decrementGripDepth, + getGlobalDebugObject +}) { + dbg_assert(!obj.optimizedOut, + "Should not create object actors for optimized out values!"); + this.obj = obj; + this.hooks = { + createValueGrip, + sources, + createEnvironmentActor, + getGripDepth, + incrementGripDepth, + decrementGripDepth, + getGlobalDebugObject + }; + this.iterators = new Set(); +} + +ObjectActor.prototype = { + actorPrefix: "obj", + + /** + * Returns a grip for this actor for returning in a protocol message. + */ + grip: function() { + this.hooks.incrementGripDepth(); + + let g = { + "type": "object", + "class": this.obj.class, + "actor": this.actorID, + "extensible": this.obj.isExtensible(), + "frozen": this.obj.isFrozen(), + "sealed": this.obj.isSealed() + }; + + if (this.obj.class != "DeadObject") { + if (this.obj.class == "Promise") { + g.promiseState = this._createPromiseState(); + } + + // FF40+: Allow to know how many properties an object has + // to lazily display them when there is a bunch. + // Throws on some MouseEvent object in tests. + try { + // Bug 1163520: Assert on internal functions + if (this.obj.class != "Function") { + g.ownPropertyLength = this.obj.getOwnPropertyNames().length; + } + } catch(e) {} + + let raw = this.obj.unsafeDereference(); + + // If Cu is not defined, we are running on a worker thread, where xrays + // don't exist. + if (Cu) { + raw = Cu.unwaiveXrays(raw); + } + + if (!DevToolsUtils.isSafeJSObject(raw)) { + raw = null; + } + + let previewers = DebuggerServer.ObjectActorPreviewers[this.obj.class] || + DebuggerServer.ObjectActorPreviewers.Object; + for (let fn of previewers) { + try { + if (fn(this, g, raw)) { + break; + } + } catch (e) { + let msg = "ObjectActor.prototype.grip previewer function"; + DevToolsUtils.reportException(msg, e); + } + } + } + + this.hooks.decrementGripDepth(); + return g; + }, + + /** + * Returns an object exposing the internal Promise state. + */ + _createPromiseState: function() { + const { state, value, reason } = getPromiseState(this.obj); + let promiseState = { state }; + let rawPromise = this.obj.unsafeDereference(); + + if (state == "fulfilled") { + promiseState.value = this.hooks.createValueGrip(value); + } else if (state == "rejected") { + promiseState.reason = this.hooks.createValueGrip(reason); + } + + promiseState.creationTimestamp = Date.now() - + PromiseDebugging.getPromiseLifetime(rawPromise); + + // If the promise is not settled, avoid adding the timeToSettle property + // and catch the error thrown by PromiseDebugging.getTimeToSettle. + try { + promiseState.timeToSettle = PromiseDebugging.getTimeToSettle(rawPromise); + } catch(e) {} + + return promiseState; + }, + + /** + * Releases this actor from the pool. + */ + release: function() { + if (this.registeredPool.objectActors) { + this.registeredPool.objectActors.delete(this.obj); + } + this.iterators.forEach(actor => this.registeredPool.removeActor(actor)); + this.iterators.clear(); + this.registeredPool.removeActor(this); + }, + + /** + * Handle a protocol request to provide the definition site of this function + * object. + */ + onDefinitionSite: function() { + if (this.obj.class != "Function") { + return { + from: this.actorID, + error: "objectNotFunction", + message: this.actorID + " is not a function." + }; + } + + if (!this.obj.script) { + return { + from: this.actorID, + error: "noScript", + message: this.actorID + " has no Debugger.Script" + }; + } + + return this.hooks.sources().getOriginalLocation(new GeneratedLocation( + this.hooks.sources().createNonSourceMappedActor(this.obj.script.source), + this.obj.script.startLine, + 0 // TODO bug 901138: use Debugger.Script.prototype.startColumn + )).then((originalLocation) => { + return { + source: originalLocation.originalSourceActor.form(), + line: originalLocation.originalLine, + column: originalLocation.originalColumn + }; + }); + }, + + /** + * Handle a protocol request to provide the names of the properties defined on + * the object and not its prototype. + */ + onOwnPropertyNames: function() { + return { from: this.actorID, + ownPropertyNames: this.obj.getOwnPropertyNames() }; + }, + + /** + * Creates an actor to iterate over an object property names and values. + * See PropertyIteratorActor constructor for more info about options param. + * + * @param request object + * The protocol request object. + */ + onEnumProperties: function(request) { + let actor = new PropertyIteratorActor(this, request.options); + this.registeredPool.addActor(actor); + this.iterators.add(actor); + return { iterator: actor.grip() }; + }, + + /** + * Handle a protocol request to provide the prototype and own properties of + * the object. + */ + onPrototypeAndProperties: function() { + let ownProperties = Object.create(null); + let names; + try { + names = this.obj.getOwnPropertyNames(); + } catch (ex) { + // The above can throw if this.obj points to a dead object. + // TODO: we should use Cu.isDeadWrapper() - see bug 885800. + return { from: this.actorID, + prototype: this.hooks.createValueGrip(null), + ownProperties: ownProperties, + safeGetterValues: Object.create(null) }; + } + for (let name of names) { + ownProperties[name] = this._propertyDescriptor(name); + } + return { from: this.actorID, + prototype: this.hooks.createValueGrip(this.obj.proto), + ownProperties: ownProperties, + safeGetterValues: this._findSafeGetterValues(names) }; + }, + + /** + * Find the safe getter values for the current Debugger.Object, |this.obj|. + * + * @private + * @param array ownProperties + * The array that holds the list of known ownProperties names for + * |this.obj|. + * @param number [limit=0] + * Optional limit of getter values to find. + * @return object + * An object that maps property names to safe getter descriptors as + * defined by the remote debugging protocol. + */ + _findSafeGetterValues: function(ownProperties, limit = 0) { + let safeGetterValues = Object.create(null); + let obj = this.obj; + let level = 0, i = 0; + + while (obj) { + let getters = this._findSafeGetters(obj); + for (let name of getters) { + // Avoid overwriting properties from prototypes closer to this.obj. Also + // avoid providing safeGetterValues from prototypes if property |name| + // is already defined as an own property. + if (name in safeGetterValues || + (obj != this.obj && ownProperties.indexOf(name) !== -1)) { + continue; + } + + // Ignore __proto__ on Object.prototye. + if (!obj.proto && name == "__proto__") { + continue; + } + + let desc = null, getter = null; + try { + desc = obj.getOwnPropertyDescriptor(name); + getter = desc.get; + } catch (ex) { + // The above can throw if the cache becomes stale. + } + if (!getter) { + obj._safeGetters = null; + continue; + } + + let result = getter.call(this.obj); + if (result && !("throw" in result)) { + let getterValue = undefined; + if ("return" in result) { + getterValue = result.return; + } else if ("yield" in result) { + getterValue = result.yield; + } + // WebIDL attributes specified with the LenientThis extended attribute + // return undefined and should be ignored. + if (getterValue !== undefined) { + safeGetterValues[name] = { + getterValue: this.hooks.createValueGrip(getterValue), + getterPrototypeLevel: level, + enumerable: desc.enumerable, + writable: level == 0 ? desc.writable : true, + }; + if (limit && ++i == limit) { + break; + } + } + } + } + if (limit && i == limit) { + break; + } + + obj = obj.proto; + level++; + } + + return safeGetterValues; + }, + + /** + * Find the safe getters for a given Debugger.Object. Safe getters are native + * getters which are safe to execute. + * + * @private + * @param Debugger.Object object + * The Debugger.Object where you want to find safe getters. + * @return Set + * A Set of names of safe getters. This result is cached for each + * Debugger.Object. + */ + _findSafeGetters: function(object) { + if (object._safeGetters) { + return object._safeGetters; + } + + let getters = new Set(); + let names = []; + try { + names = object.getOwnPropertyNames() + } catch (ex) { + // Calling getOwnPropertyNames() on some wrapped native prototypes is not + // allowed: "cannot modify properties of a WrappedNative". See bug 952093. + } + + for (let name of names) { + let desc = null; + try { + desc = object.getOwnPropertyDescriptor(name); + } catch (e) { + // Calling getOwnPropertyDescriptor on wrapped native prototypes is not + // allowed (bug 560072). + } + if (!desc || desc.value !== undefined || !("get" in desc)) { + continue; + } + + if (DevToolsUtils.hasSafeGetter(desc)) { + getters.add(name); + } + } + + object._safeGetters = getters; + return getters; + }, + + /** + * Handle a protocol request to provide the prototype of the object. + */ + onPrototype: function() { + return { from: this.actorID, + prototype: this.hooks.createValueGrip(this.obj.proto) }; + }, + + /** + * Handle a protocol request to provide the property descriptor of the + * object's specified property. + * + * @param request object + * The protocol request object. + */ + onProperty: function(request) { + if (!request.name) { + return { error: "missingParameter", + message: "no property name was specified" }; + } + + return { from: this.actorID, + descriptor: this._propertyDescriptor(request.name) }; + }, + + /** + * Handle a protocol request to provide the display string for the object. + */ + onDisplayString: function() { + const string = stringify(this.obj); + return { from: this.actorID, + displayString: this.hooks.createValueGrip(string) }; + }, + + /** + * A helper method that creates a property descriptor for the provided object, + * properly formatted for sending in a protocol response. + * + * @private + * @param string name + * The property that the descriptor is generated for. + * @param boolean [onlyEnumerable] + * Optional: true if you want a descriptor only for an enumerable + * property, false otherwise. + * @return object|undefined + * The property descriptor, or undefined if this is not an enumerable + * property and onlyEnumerable=true. + */ + _propertyDescriptor: function(name, onlyEnumerable) { + let desc; + try { + desc = this.obj.getOwnPropertyDescriptor(name); + } catch (e) { + // Calling getOwnPropertyDescriptor on wrapped native prototypes is not + // allowed (bug 560072). Inform the user with a bogus, but hopefully + // explanatory, descriptor. + return { + configurable: false, + writable: false, + enumerable: false, + value: e.name + }; + } + + if (!desc || onlyEnumerable && !desc.enumerable) { + return undefined; + } + + let retval = { + configurable: desc.configurable, + enumerable: desc.enumerable + }; + + if ("value" in desc) { + retval.writable = desc.writable; + retval.value = this.hooks.createValueGrip(desc.value); + } else { + if ("get" in desc) { + retval.get = this.hooks.createValueGrip(desc.get); + } + if ("set" in desc) { + retval.set = this.hooks.createValueGrip(desc.set); + } + } + return retval; + }, + + /** + * Handle a protocol request to provide the source code of a function. + * + * @param request object + * The protocol request object. + */ + onDecompile: function(request) { + if (this.obj.class !== "Function") { + return { error: "objectNotFunction", + message: "decompile request is only valid for object grips " + + "with a 'Function' class." }; + } + + return { from: this.actorID, + decompiledCode: this.obj.decompile(!!request.pretty) }; + }, + + /** + * Handle a protocol request to provide the parameters of a function. + */ + onParameterNames: function() { + if (this.obj.class !== "Function") { + return { error: "objectNotFunction", + message: "'parameterNames' request is only valid for object " + + "grips with a 'Function' class." }; + } + + return { parameterNames: this.obj.parameterNames }; + }, + + /** + * Handle a protocol request to release a thread-lifetime grip. + */ + onRelease: function() { + this.release(); + return {}; + }, + + /** + * Handle a protocol request to provide the lexical scope of a function. + */ + onScope: function() { + if (this.obj.class !== "Function") { + return { error: "objectNotFunction", + message: "scope request is only valid for object grips with a" + + " 'Function' class." }; + } + + let envActor = this.hooks.createEnvironmentActor(this.obj.environment, + this.registeredPool); + if (!envActor) { + return { error: "notDebuggee", + message: "cannot access the environment of this function." }; + } + + return { from: this.actorID, scope: envActor.form() }; + }, + + /** + * Handle a protocol request to get the list of dependent promises of a + * promise. + * + * @return object + * Returns an object containing an array of object grips of the + * dependent promises + */ + onDependentPromises: function() { + if (this.obj.class != "Promise") { + return { error: "objectNotPromise", + message: "'dependentPromises' request is only valid for " + + "object grips with a 'Promise' class." }; + } + + let rawPromise = this.obj.unsafeDereference(); + let promises = PromiseDebugging.getDependentPromises(rawPromise).map(p => + this.hooks.createValueGrip(this.obj.makeDebuggeeValue(p))); + + return { promises }; + }, + + /** + * Handle a protocol request to get the allocation stack of a promise. + */ + onAllocationStack: function() { + if (this.obj.class != "Promise") { + return { error: "objectNotPromise", + message: "'allocationStack' request is only valid for " + + "object grips with a 'Promise' class." }; + } + + let rawPromise = this.obj.unsafeDereference(); + let stack = PromiseDebugging.getAllocationStack(rawPromise); + let allocationStacks = []; + + while (stack) { + if (stack.source) { + let source = this._getSourceOriginalLocation(stack); + + if (source) { + allocationStacks.push(source); + } + } + stack = stack.parent; + } + + return Promise.all(allocationStacks).then(stacks => { + return { allocationStack: stacks }; + }); + }, + + /** + * Helper function for onAllocationStack which fetches the source location + * for a SavedFrame stack. + * @param SavedFrame stack + * The promise allocation stack frame + * @return object + * Returns an object containing the source location of the SavedFrame + * stack. + */ + _getSourceOriginalLocation: function(stack) { + let source; + + // Catch any errors if the source actor cannot be found + try { + source = this.hooks.sources().getSourceActorByURL(stack.source); + } catch(e) {} + + if (!source) { + return null; + } + + return this.hooks.sources().getOriginalLocation(new GeneratedLocation( + source, + stack.line, + stack.column + )).then((originalLocation) => { + return { + source: originalLocation.originalSourceActor.form(), + line: originalLocation.originalLine, + column: originalLocation.originalColumn, + functionDisplayName: stack.functionDisplayName + }; + }); + } +}; + +ObjectActor.prototype.requestTypes = { + "definitionSite": ObjectActor.prototype.onDefinitionSite, + "parameterNames": ObjectActor.prototype.onParameterNames, + "prototypeAndProperties": ObjectActor.prototype.onPrototypeAndProperties, + "enumProperties": ObjectActor.prototype.onEnumProperties, + "prototype": ObjectActor.prototype.onPrototype, + "property": ObjectActor.prototype.onProperty, + "displayString": ObjectActor.prototype.onDisplayString, + "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames, + "decompile": ObjectActor.prototype.onDecompile, + "release": ObjectActor.prototype.onRelease, + "scope": ObjectActor.prototype.onScope, + "dependentPromises": ObjectActor.prototype.onDependentPromises, + "allocationStack": ObjectActor.prototype.onAllocationStack +}; + +/** + * Creates an actor to iterate over an object's property names and values. + * + * @param objectActor ObjectActor + * The object actor. + * @param options Object + * A dictionary object with various boolean attributes: + * - ignoreSafeGetters Boolean + * If true, do not iterate over safe getters. + * - ignoreIndexedProperties Boolean + * If true, filters out Array items. + * e.g. properties names between `0` and `object.length`. + * - ignoreNonIndexedProperties Boolean + * If true, filters out items that aren't array items + * e.g. properties names that are not a number between `0` + * and `object.length`. + * - sort Boolean + * If true, the iterator will sort the properties by name + * before dispatching them. + * - query String + * If non-empty, will filter the properties by names containing + * this query string. The match is not case-sensitive. + */ +function PropertyIteratorActor(objectActor, options){ + this.objectActor = objectActor; + + let ownProperties = Object.create(null); + let names = []; + try { + names = this.objectActor.obj.getOwnPropertyNames(); + } catch (ex) {} + + let safeGetterValues = {}; + let safeGetterNames = []; + if (!options.ignoreSafeGetters) { + // Merge the safe getter values into the existing properties list. + safeGetterValues = this.objectActor._findSafeGetterValues(names); + safeGetterNames = Object.keys(safeGetterValues); + for (let name of safeGetterNames) { + if (names.indexOf(name) === -1) { + names.push(name); + } + } + } + + if (options.ignoreIndexedProperties || options.ignoreNonIndexedProperties) { + let length = DevToolsUtils.getProperty(this.objectActor.obj, "length"); + if (typeof(length) !== "number") { + // Pseudo arrays are flagged as ArrayLike if they have + // subsequent indexed properties without having any length attribute. + length = 0; + for (let key of names) { + if (isNaN(key) || key != length++) { + break; + } + } + } + + if (options.ignoreIndexedProperties) { + names = names.filter(i => { + // Use parseFloat in order to reject floats... + // (parseInt converts floats to integer) + // (Number(str) converts spaces to 0) + i = parseFloat(i); + return !Number.isInteger(i) || i < 0 || i >= length; + }); + } + + if (options.ignoreNonIndexedProperties) { + names = names.filter(i => { + i = parseFloat(i); + return Number.isInteger(i) && i >= 0 && i < length; + }); + } + } + + if (options.query) { + let { query } = options; + query = query.toLowerCase(); + names = names.filter(name => { + return name.toLowerCase().includes(query); + }); + } + + if (options.sort) { + names.sort(); + } + + // Now build the descriptor list + for (let name of names) { + let desc = this.objectActor._propertyDescriptor(name); + if (!desc) { + desc = safeGetterValues[name]; + } + else if (name in safeGetterValues) { + // Merge the safe getter values into the existing properties list. + let { getterValue, getterPrototypeLevel } = safeGetterValues[name]; + desc.getterValue = getterValue; + desc.getterPrototypeLevel = getterPrototypeLevel; + } + ownProperties[name] = desc; + } + + this.names = names; + this.ownProperties = ownProperties; +} + +PropertyIteratorActor.prototype = { + actorPrefix: "propertyIterator", + + grip: function() { + return { + type: "propertyIterator", + actor: this.actorID, + count: this.names.length + }; + }, + + names: function({ indexes }) { + let list = []; + for (let idx of indexes) { + list.push(this.names[idx]); + } + return { + names: list + }; + }, + + slice: function({ start, count }) { + let names = this.names.slice(start, start + count); + let props = Object.create(null); + for (let name of names) { + props[name] = this.ownProperties[name]; + } + return { + ownProperties: props + }; + }, + + all: function() { + return { + ownProperties: this.ownProperties + }; + } +}; + +PropertyIteratorActor.prototype.requestTypes = { + "names": PropertyIteratorActor.prototype.names, + "slice": PropertyIteratorActor.prototype.slice, + "all": PropertyIteratorActor.prototype.all, +}; + +/** + * Functions for adding information to ObjectActor grips for the purpose of + * having customized output. This object holds arrays mapped by + * Debugger.Object.prototype.class. + * + * In each array you can add functions that take two + * arguments: + * - the ObjectActor instance and its hooks to make a preview for, + * - the grip object being prepared for the client, + * - the raw JS object after calling Debugger.Object.unsafeDereference(). This + * argument is only provided if the object is safe for reading properties and + * executing methods. See DevToolsUtils.isSafeJSObject(). + * + * Functions must return false if they cannot provide preview + * information for the debugger object, or true otherwise. + */ +DebuggerServer.ObjectActorPreviewers = { + String: [function({obj, hooks}, grip) { + let result = genericObjectPreviewer("String", String, obj, hooks); + let length = DevToolsUtils.getProperty(obj, "length"); + + if (!result || typeof length != "number") { + return false; + } + + grip.preview = { + kind: "ArrayLike", + length: length + }; + + if (hooks.getGripDepth() > 1) { + return true; + } + + let items = grip.preview.items = []; + + const max = Math.min(result.value.length, OBJECT_PREVIEW_MAX_ITEMS); + for (let i = 0; i < max; i++) { + let value = hooks.createValueGrip(result.value[i]); + items.push(value); + } + + return true; + }], + + Boolean: [function({obj, hooks}, grip) { + let result = genericObjectPreviewer("Boolean", Boolean, obj, hooks); + if (result) { + grip.preview = result; + return true; + } + + return false; + }], + + Number: [function({obj, hooks}, grip) { + let result = genericObjectPreviewer("Number", Number, obj, hooks); + if (result) { + grip.preview = result; + return true; + } + + return false; + }], + + Function: [function({obj, hooks}, grip) { + if (obj.name) { + grip.name = obj.name; + } + + if (obj.displayName) { + grip.displayName = obj.displayName.substr(0, 500); + } + + if (obj.parameterNames) { + grip.parameterNames = obj.parameterNames; + } + + // Check if the developer has added a de-facto standard displayName + // property for us to use. + let userDisplayName; + try { + userDisplayName = obj.getOwnPropertyDescriptor("displayName"); + } catch (e) { + // Calling getOwnPropertyDescriptor with displayName might throw + // with "permission denied" errors for some functions. + dumpn(e); + } + + if (userDisplayName && typeof userDisplayName.value == "string" && + userDisplayName.value) { + grip.userDisplayName = hooks.createValueGrip(userDisplayName.value); + } + + let dbgGlobal = hooks.getGlobalDebugObject(); + if (dbgGlobal) { + let script = dbgGlobal.makeDebuggeeValue(obj.unsafeDereference()).script; + if (script) { + grip.location = { + url: script.url, + line: script.startLine + }; + } + } + + return true; + }], + + RegExp: [function({obj, hooks}, grip) { + // Avoid having any special preview for the RegExp.prototype itself. + if (!obj.proto || obj.proto.class != "RegExp") { + return false; + } + + let str = RegExp.prototype.toString.call(obj.unsafeDereference()); + grip.displayString = hooks.createValueGrip(str); + return true; + }], + + Date: [function({obj, hooks}, grip) { + let time = Date.prototype.getTime.call(obj.unsafeDereference()); + + grip.preview = { + timestamp: hooks.createValueGrip(time), + }; + return true; + }], + + Array: [function({obj, hooks}, grip) { + let length = DevToolsUtils.getProperty(obj, "length"); + if (typeof length != "number") { + return false; + } + + grip.preview = { + kind: "ArrayLike", + length: length, + }; + + if (hooks.getGripDepth() > 1) { + return true; + } + + let raw = obj.unsafeDereference(); + let items = grip.preview.items = []; + + for (let i = 0; i < length; ++i) { + // Array Xrays filter out various possibly-unsafe properties (like + // functions, and claim that the value is undefined instead. This + // is generally the right thing for privileged code accessing untrusted + // objects, but quite confusing for Object previews. So we manually + // override this protection by waiving Xrays on the array, and re-applying + // Xrays on any indexed value props that we pull off of it. + let desc = Object.getOwnPropertyDescriptor(Cu.waiveXrays(raw), i); + if (desc && !desc.get && !desc.set) { + let value = Cu.unwaiveXrays(desc.value); + value = makeDebuggeeValueIfNeeded(obj, value); + items.push(hooks.createValueGrip(value)); + } else { + items.push(null); + } + + if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }], + + Set: [function({obj, hooks}, grip) { + let size = DevToolsUtils.getProperty(obj, "size"); + if (typeof size != "number") { + return false; + } + + grip.preview = { + kind: "ArrayLike", + length: size, + }; + + // Avoid recursive object grips. + if (hooks.getGripDepth() > 1) { + return true; + } + + let raw = obj.unsafeDereference(); + let items = grip.preview.items = []; + // We currently lack XrayWrappers for Set, so when we iterate over + // the values, the temporary iterator objects get created in the target + // compartment. However, we _do_ have Xrays to Object now, so we end up + // Xraying those temporary objects, and filtering access to |it.value| + // based on whether or not it's Xrayable and/or callable, which breaks + // the for/of iteration. + // + // This code is designed to handle untrusted objects, so we can safely + // waive Xrays on the iterable, and relying on the Debugger machinery to + // make sure we handle the resulting objects carefully. + for (let item of Cu.waiveXrays(Set.prototype.values.call(raw))) { + item = Cu.unwaiveXrays(item); + item = makeDebuggeeValueIfNeeded(obj, item); + items.push(hooks.createValueGrip(item)); + if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }], + + Map: [function({obj, hooks}, grip) { + let size = DevToolsUtils.getProperty(obj, "size"); + if (typeof size != "number") { + return false; + } + + grip.preview = { + kind: "MapLike", + size: size, + }; + + if (hooks.getGripDepth() > 1) { + return true; + } + + let raw = obj.unsafeDereference(); + let entries = grip.preview.entries = []; + // Iterating over a Map via .entries goes through various intermediate + // objects - an Iterator object, then a 2-element Array object, then the + // actual values we care about. We don't have Xrays to Iterator objects, + // so we get Opaque wrappers for them. And even though we have Xrays to + // Arrays, the semantics often deny access to the entires based on the + // nature of the values. So we need waive Xrays for the iterator object + // and the tupes, and then re-apply them on the underlying values until + // we fix bug 1023984. + // + // Even then though, we might want to continue waiving Xrays here for the + // same reason we do so for Arrays above - this filtering behavior is likely + // to be more confusing than beneficial in the case of Object previews. + for (let keyValuePair of Cu.waiveXrays(Map.prototype.entries.call(raw))) { + let key = Cu.unwaiveXrays(keyValuePair[0]); + let value = Cu.unwaiveXrays(keyValuePair[1]); + key = makeDebuggeeValueIfNeeded(obj, key); + value = makeDebuggeeValueIfNeeded(obj, value); + entries.push([hooks.createValueGrip(key), + hooks.createValueGrip(value)]); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }], + + DOMStringMap: [function({obj, hooks}, grip, rawObj) { + if (!rawObj) { + return false; + } + + let keys = obj.getOwnPropertyNames(); + grip.preview = { + kind: "MapLike", + size: keys.length, + }; + + if (hooks.getGripDepth() > 1) { + return true; + } + + let entries = grip.preview.entries = []; + for (let key of keys) { + let value = makeDebuggeeValueIfNeeded(obj, rawObj[key]); + entries.push([key, hooks.createValueGrip(value)]); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }], +}; + +/** + * Generic previewer for "simple" classes like String, Number and Boolean. + * + * @param string className + * Class name to expect. + * @param object classObj + * The class to expect, eg. String. The valueOf() method of the class is + * invoked on the given object. + * @param Debugger.Object obj + * The debugger object we need to preview. + * @param object hooks + * The thread actor to use to create a value grip. + * @return object|null + * An object with one property, "value", which holds the value grip that + * represents the given object. Null is returned if we cant preview the + * object. + */ +function genericObjectPreviewer(className, classObj, obj, hooks) { + if (!obj.proto || obj.proto.class != className) { + return null; + } + + let raw = obj.unsafeDereference(); + let v = null; + try { + v = classObj.prototype.valueOf.call(raw); + } catch (ex) { + // valueOf() can throw if the raw JS object is "misbehaved". + return null; + } + + if (v !== null) { + v = hooks.createValueGrip(makeDebuggeeValueIfNeeded(obj, v)); + return { value: v }; + } + + return null; +} + +// Preview functions that do not rely on the object class. +DebuggerServer.ObjectActorPreviewers.Object = [ + function TypedArray({obj, hooks}, grip) { + if (TYPED_ARRAY_CLASSES.indexOf(obj.class) == -1) { + return false; + } + + let length = DevToolsUtils.getProperty(obj, "length"); + if (typeof length != "number") { + return false; + } + + grip.preview = { + kind: "ArrayLike", + length: length, + }; + + if (hooks.getGripDepth() > 1) { + return true; + } + + let raw = obj.unsafeDereference(); + let global = Cu.getGlobalForObject(DebuggerServer); + let classProto = global[obj.class].prototype; + // The Xray machinery for TypedArrays denies indexed access on the grounds + // that it's slow, and advises callers to do a structured clone instead. + let safeView = Cu.cloneInto(classProto.subarray.call(raw, 0, + OBJECT_PREVIEW_MAX_ITEMS), global); + let items = grip.preview.items = []; + for (let i = 0; i < safeView.length; i++) { + items.push(safeView[i]); + } + + return true; + }, + + function Error({obj, hooks}, grip) { + switch (obj.class) { + case "Error": + case "EvalError": + case "RangeError": + case "ReferenceError": + case "SyntaxError": + case "TypeError": + case "URIError": + let name = DevToolsUtils.getProperty(obj, "name"); + let msg = DevToolsUtils.getProperty(obj, "message"); + let stack = DevToolsUtils.getProperty(obj, "stack"); + let fileName = DevToolsUtils.getProperty(obj, "fileName"); + let lineNumber = DevToolsUtils.getProperty(obj, "lineNumber"); + let columnNumber = DevToolsUtils.getProperty(obj, "columnNumber"); + grip.preview = { + kind: "Error", + name: hooks.createValueGrip(name), + message: hooks.createValueGrip(msg), + stack: hooks.createValueGrip(stack), + fileName: hooks.createValueGrip(fileName), + lineNumber: hooks.createValueGrip(lineNumber), + columnNumber: hooks.createValueGrip(columnNumber), + }; + return true; + default: + return false; + } + }, + + function CSSMediaRule({obj, hooks}, grip, rawObj) { + if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMCSSMediaRule)) { + return false; + } + grip.preview = { + kind: "ObjectWithText", + text: hooks.createValueGrip(rawObj.conditionText), + }; + return true; + }, + + function CSSStyleRule({obj, hooks}, grip, rawObj) { + if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMCSSStyleRule)) { + return false; + } + grip.preview = { + kind: "ObjectWithText", + text: hooks.createValueGrip(rawObj.selectorText), + }; + return true; + }, + + function ObjectWithURL({obj, hooks}, grip, rawObj) { + if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMCSSImportRule || + rawObj instanceof Ci.nsIDOMCSSStyleSheet || + rawObj instanceof Ci.nsIDOMLocation || + rawObj instanceof Ci.nsIDOMWindow)) { + return false; + } + + let url; + if (rawObj instanceof Ci.nsIDOMWindow && rawObj.location) { + url = rawObj.location.href; + } else if (rawObj.href) { + url = rawObj.href; + } else { + return false; + } + + grip.preview = { + kind: "ObjectWithURL", + url: hooks.createValueGrip(url), + }; + + return true; + }, + + function ArrayLike({obj, hooks}, grip, rawObj) { + if (isWorker || !rawObj || + obj.class != "DOMStringList" && + obj.class != "DOMTokenList" && + !(rawObj instanceof Ci.nsIDOMMozNamedAttrMap || + rawObj instanceof Ci.nsIDOMCSSRuleList || + rawObj instanceof Ci.nsIDOMCSSValueList || + rawObj instanceof Ci.nsIDOMFileList || + rawObj instanceof Ci.nsIDOMFontFaceList || + rawObj instanceof Ci.nsIDOMMediaList || + rawObj instanceof Ci.nsIDOMNodeList || + rawObj instanceof Ci.nsIDOMStyleSheetList)) { + return false; + } + + if (typeof rawObj.length != "number") { + return false; + } + + grip.preview = { + kind: "ArrayLike", + length: rawObj.length, + }; + + if (hooks.getGripDepth() > 1) { + return true; + } + + let items = grip.preview.items = []; + + for (let i = 0; i < rawObj.length && + items.length < OBJECT_PREVIEW_MAX_ITEMS; i++) { + let value = makeDebuggeeValueIfNeeded(obj, rawObj[i]); + items.push(hooks.createValueGrip(value)); + } + + return true; + }, + + function CSSStyleDeclaration({obj, hooks}, grip, rawObj) { + if (isWorker || !rawObj || + !(rawObj instanceof Ci.nsIDOMCSSStyleDeclaration)) { + return false; + } + + grip.preview = { + kind: "MapLike", + size: rawObj.length, + }; + + let entries = grip.preview.entries = []; + + for (let i = 0; i < OBJECT_PREVIEW_MAX_ITEMS && + i < rawObj.length; i++) { + let prop = rawObj[i]; + let value = rawObj.getPropertyValue(prop); + entries.push([prop, hooks.createValueGrip(value)]); + } + + return true; + }, + + function DOMNode({obj, hooks}, grip, rawObj) { + if (isWorker || obj.class == "Object" || !rawObj || + !(rawObj instanceof Ci.nsIDOMNode)) { + return false; + } + + let preview = grip.preview = { + kind: "DOMNode", + nodeType: rawObj.nodeType, + nodeName: rawObj.nodeName, + }; + + if (rawObj instanceof Ci.nsIDOMDocument && rawObj.location) { + preview.location = hooks.createValueGrip(rawObj.location.href); + } else if (rawObj instanceof Ci.nsIDOMDocumentFragment) { + preview.childNodesLength = rawObj.childNodes.length; + + if (hooks.getGripDepth() < 2) { + preview.childNodes = []; + for (let node of rawObj.childNodes) { + let actor = hooks.createValueGrip(obj.makeDebuggeeValue(node)); + preview.childNodes.push(actor); + if (preview.childNodes.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + } + } else if (rawObj instanceof Ci.nsIDOMElement) { + // Add preview for DOM element attributes. + if (rawObj instanceof Ci.nsIDOMHTMLElement) { + preview.nodeName = preview.nodeName.toLowerCase(); + } + + let i = 0; + preview.attributes = {}; + preview.attributesLength = rawObj.attributes.length; + for (let attr of rawObj.attributes) { + preview.attributes[attr.nodeName] = hooks.createValueGrip(attr.value); + if (++i == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + } else if (rawObj instanceof Ci.nsIDOMAttr) { + preview.value = hooks.createValueGrip(rawObj.value); + } else if (rawObj instanceof Ci.nsIDOMText || + rawObj instanceof Ci.nsIDOMComment) { + preview.textContent = hooks.createValueGrip(rawObj.textContent); + } + + return true; + }, + + function DOMEvent({obj, hooks}, grip, rawObj) { + if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMEvent)) { + return false; + } + + let preview = grip.preview = { + kind: "DOMEvent", + type: rawObj.type, + properties: Object.create(null), + }; + + if (hooks.getGripDepth() < 2) { + let target = obj.makeDebuggeeValue(rawObj.target); + preview.target = hooks.createValueGrip(target); + } + + let props = []; + if (rawObj instanceof Ci.nsIDOMMouseEvent) { + props.push("buttons", "clientX", "clientY", "layerX", "layerY"); + } else if (rawObj instanceof Ci.nsIDOMKeyEvent) { + let modifiers = []; + if (rawObj.altKey) { + modifiers.push("Alt"); + } + if (rawObj.ctrlKey) { + modifiers.push("Control"); + } + if (rawObj.metaKey) { + modifiers.push("Meta"); + } + if (rawObj.shiftKey) { + modifiers.push("Shift"); + } + preview.eventKind = "key"; + preview.modifiers = modifiers; + + props.push("key", "charCode", "keyCode"); + } else if (rawObj instanceof Ci.nsIDOMTransitionEvent) { + props.push("propertyName", "pseudoElement"); + } else if (rawObj instanceof Ci.nsIDOMAnimationEvent) { + props.push("animationName", "pseudoElement"); + } else if (rawObj instanceof Ci.nsIDOMClipboardEvent) { + props.push("clipboardData"); + } + + // Add event-specific properties. + for (let prop of props) { + let value = rawObj[prop]; + if (value && (typeof value == "object" || typeof value == "function")) { + // Skip properties pointing to objects. + if (hooks.getGripDepth() > 1) { + continue; + } + value = obj.makeDebuggeeValue(value); + } + preview.properties[prop] = hooks.createValueGrip(value); + } + + // Add any properties we find on the event object. + if (!props.length) { + let i = 0; + for (let prop in rawObj) { + let value = rawObj[prop]; + if (prop == "target" || prop == "type" || value === null || + typeof value == "function") { + continue; + } + if (value && typeof value == "object") { + if (hooks.getGripDepth() > 1) { + continue; + } + value = obj.makeDebuggeeValue(value); + } + preview.properties[prop] = hooks.createValueGrip(value); + if (++i == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + } + + return true; + }, + + function DOMException({obj, hooks}, grip, rawObj) { + if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMDOMException)) { + return false; + } + + grip.preview = { + kind: "DOMException", + name: hooks.createValueGrip(rawObj.name), + message: hooks.createValueGrip(rawObj.message), + code: hooks.createValueGrip(rawObj.code), + result: hooks.createValueGrip(rawObj.result), + filename: hooks.createValueGrip(rawObj.filename), + lineNumber: hooks.createValueGrip(rawObj.lineNumber), + columnNumber: hooks.createValueGrip(rawObj.columnNumber), + }; + + return true; + }, + + function PseudoArray({obj, hooks}, grip, rawObj) { + let length = 0; + + // Making sure all keys are numbers from 0 to length-1 + let keys = obj.getOwnPropertyNames(); + if (keys.length == 0) { + return false; + } + for (let key of keys) { + if (isNaN(key) || key != length++) { + return false; + } + } + + grip.preview = { + kind: "ArrayLike", + length: length, + }; + + // Avoid recursive object grips. + if (hooks.getGripDepth() > 1) { + return true; + } + + let items = grip.preview.items = []; + + let i = 0; + for (let key of keys) { + if (rawObj.hasOwnProperty(key) && i++ < OBJECT_PREVIEW_MAX_ITEMS) { + let value = makeDebuggeeValueIfNeeded(obj, rawObj[key]); + items.push(hooks.createValueGrip(value)); + } + } + + return true; + }, + + function GenericObject(objectActor, grip) { + let {obj, hooks} = objectActor; + if (grip.preview || grip.displayString || hooks.getGripDepth() > 1) { + return false; + } + + let i = 0, names = []; + let preview = grip.preview = { + kind: "Object", + ownProperties: Object.create(null), + }; + + try { + names = obj.getOwnPropertyNames(); + } catch (ex) { + // Calling getOwnPropertyNames() on some wrapped native prototypes is not + // allowed: "cannot modify properties of a WrappedNative". See bug 952093. + } + + preview.ownPropertiesLength = names.length; + + for (let name of names) { + let desc = objectActor._propertyDescriptor(name, true); + if (!desc) { + continue; + } + + preview.ownProperties[name] = desc; + if (++i == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + if (i < OBJECT_PREVIEW_MAX_ITEMS) { + preview.safeGetterValues = objectActor._findSafeGetterValues( + Object.keys(preview.ownProperties), + OBJECT_PREVIEW_MAX_ITEMS - i); + } + + return true; + }, +]; + +/** + * Call PromiseDebugging.getState on this Debugger.Object's referent and wrap + * the resulting `value` or `reason` properties in a Debugger.Object instance. + * + * See dom/webidl/PromiseDebugging.webidl + * + * @returns Object + * An object of one of the following forms: + * - { state: "pending" } + * - { state: "fulfilled", value } + * - { state: "rejected", reason } + */ +function getPromiseState(obj) { + if (obj.class != "Promise") { + throw new Error( + "Can't call `getPromiseState` on `Debugger.Object`s that don't " + + "refer to Promise objects."); + } + + const state = PromiseDebugging.getState(obj.unsafeDereference()); + return { + state: state.state, + value: obj.makeDebuggeeValue(state.value), + reason: obj.makeDebuggeeValue(state.reason) + }; +}; + +/** + * Determine if a given value is non-primitive. + * + * @param Any value + * The value to test. + * @return Boolean + * Whether the value is non-primitive. + */ +function isObject(value) { + const type = typeof value; + return type == "object" ? value !== null : type == "function"; +} + +/** + * Create a function that can safely stringify Debugger.Objects of a given + * builtin type. + * + * @param Function ctor + * The builtin class constructor. + * @return Function + * The stringifier for the class. + */ +function createBuiltinStringifier(ctor) { + return obj => ctor.prototype.toString.call(obj.unsafeDereference()); +} + +/** + * Stringify a Debugger.Object-wrapped Error instance. + * + * @param Debugger.Object obj + * The object to stringify. + * @return String + * The stringification of the object. + */ +function errorStringify(obj) { + let name = DevToolsUtils.getProperty(obj, "name"); + if (name === "" || name === undefined) { + name = obj.class; + } else if (isObject(name)) { + name = stringify(name); + } + + let message = DevToolsUtils.getProperty(obj, "message"); + if (isObject(message)) { + message = stringify(message); + } + + if (message === "" || message === undefined) { + return name; + } + return name + ": " + message; +} + +/** + * Stringify a Debugger.Object based on its class. + * + * @param Debugger.Object obj + * The object to stringify. + * @return String + * The stringification for the object. + */ +function stringify(obj) { + if (obj.class == "DeadObject") { + const error = new Error("Dead object encountered."); + DevToolsUtils.reportException("stringify", error); + return ""; + } + + const stringifier = stringifiers[obj.class] || stringifiers.Object; + + try { + return stringifier(obj); + } catch (e) { + DevToolsUtils.reportException("stringify", e); + return ""; + } +} + +// Used to prevent infinite recursion when an array is found inside itself. +let seen = null; + +let stringifiers = { + Error: errorStringify, + EvalError: errorStringify, + RangeError: errorStringify, + ReferenceError: errorStringify, + SyntaxError: errorStringify, + TypeError: errorStringify, + URIError: errorStringify, + Boolean: createBuiltinStringifier(Boolean), + Function: createBuiltinStringifier(Function), + Number: createBuiltinStringifier(Number), + RegExp: createBuiltinStringifier(RegExp), + String: createBuiltinStringifier(String), + Object: obj => "[object " + obj.class + "]", + Array: obj => { + // If we're at the top level then we need to create the Set for tracking + // previously stringified arrays. + const topLevel = !seen; + if (topLevel) { + seen = new Set(); + } else if (seen.has(obj)) { + return ""; + } + + seen.add(obj); + + const len = DevToolsUtils.getProperty(obj, "length"); + let string = ""; + + // The following check is only required because the debuggee could possibly + // be a Proxy and return any value. For normal objects, array.length is + // always a non-negative integer. + if (typeof len == "number" && len > 0) { + for (let i = 0; i < len; i++) { + const desc = obj.getOwnPropertyDescriptor(i); + if (desc) { + const { value } = desc; + if (value != null) { + string += isObject(value) ? stringify(value) : value; + } + } + + if (i < len - 1) { + string += ","; + } + } + } + + if (topLevel) { + seen = null; + } + + return string; + }, + DOMException: obj => { + const message = DevToolsUtils.getProperty(obj, "message") || ""; + const result = (+DevToolsUtils.getProperty(obj, "result")).toString(16); + const code = DevToolsUtils.getProperty(obj, "code"); + const name = DevToolsUtils.getProperty(obj, "name") || ""; + + return '[Exception... "' + message + '" ' + + 'code: "' + code +'" ' + + 'nsresult: "0x' + result + ' (' + name + ')"]'; + }, + Promise: obj => { + const { state, value, reason } = getPromiseState(obj); + let statePreview = state; + if (state != "pending") { + const settledValue = state === "fulfilled" ? value : reason; + statePreview += ": " + (typeof settledValue === "object" && settledValue !== null + ? stringify(settledValue) + : settledValue); + } + return "Promise (" + statePreview + ")"; + }, +}; + +/** + * Make a debuggee value for the given object, if needed. Primitive values + * are left the same. + * + * Use case: you have a raw JS object (after unsafe dereference) and you want to + * send it to the client. In that case you need to use an ObjectActor which + * requires a debuggee value. The Debugger.Object.prototype.makeDebuggeeValue() + * method works only for JS objects and functions. + * + * @param Debugger.Object obj + * @param any value + * @return object + */ +function makeDebuggeeValueIfNeeded(obj, value) { + if (value && (typeof value == "object" || typeof value == "function")) { + return obj.makeDebuggeeValue(value); + } + return value; +} + +/** + * Creates an actor for the specied "very long" string. "Very long" is specified + * at the server's discretion. + * + * @param string String + * The string. + */ +function LongStringActor(string) { + this.string = string; + this.stringLength = string.length; +} + +LongStringActor.prototype = { + actorPrefix: "longString", + + disconnect: function() { + // Because longStringActors is not a weak map, we won't automatically leave + // it so we need to manually leave on disconnect so that we don't leak + // memory. + this._releaseActor(); + }, + + /** + * Returns a grip for this actor for returning in a protocol message. + */ + grip: function() { + return { + "type": "longString", + "initial": this.string.substring( + 0, DebuggerServer.LONG_STRING_INITIAL_LENGTH), + "length": this.stringLength, + "actor": this.actorID + }; + }, + + /** + * Handle a request to extract part of this actor's string. + * + * @param request object + * The protocol request object. + */ + onSubstring: function(request) { + return { + "from": this.actorID, + "substring": this.string.substring(request.start, request.end) + }; + }, + + /** + * Handle a request to release this LongStringActor instance. + */ + onRelease: function () { + // TODO: also check if registeredPool === threadActor.threadLifetimePool + // when the web console moves aray from manually releasing pause-scoped + // actors. + this._releaseActor(); + this.registeredPool.removeActor(this); + return {}; + }, + + _releaseActor: function() { + if (this.registeredPool && this.registeredPool.longStringActors) { + delete this.registeredPool.longStringActors[this.string]; + } + } +}; + +LongStringActor.prototype.requestTypes = { + "substring": LongStringActor.prototype.onSubstring, + "release": LongStringActor.prototype.onRelease +}; + +/** + * Create a grip for the given debuggee value. If the value is an + * object, will create an actor with the given lifetime. + */ +function createValueGrip(value, pool, makeObjectGrip) { + switch (typeof value) { + case "boolean": + return value; + + case "string": + if (stringIsLong(value)) { + return longStringGrip(value, pool); + } + return value; + + case "number": + if (value === Infinity) { + return { type: "Infinity" }; + } else if (value === -Infinity) { + return { type: "-Infinity" }; + } else if (Number.isNaN(value)) { + return { type: "NaN" }; + } else if (!value && 1 / value === -Infinity) { + return { type: "-0" }; + } + return value; + + case "undefined": + return { type: "undefined" }; + + case "object": + if (value === null) { + return { type: "null" }; + } + else if(value.optimizedOut || + value.uninitialized || + value.missingArguments) { + // The slot is optimized out, an uninitialized binding, or + // arguments on a dead scope + return { + type: "null", + optimizedOut: value.optimizedOut, + uninitialized: value.uninitialized, + missingArguments: value.missingArguments + }; + } + return makeObjectGrip(value, pool); + + case "symbol": + let form = { + type: "symbol" + }; + let name = getSymbolName(value); + if (name !== undefined) { + form.name = createValueGrip(name, pool, makeObjectGrip); + } + return form; + + default: + dbg_assert(false, "Failed to provide a grip for: " + value); + return null; + } +} + +const symbolProtoToString = Symbol.prototype.toString; + +function getSymbolName(symbol) { + const name = symbolProtoToString.call(symbol).slice("Symbol(".length, -1); + return name || undefined; +} + +/** + * Returns true if the string is long enough to use a LongStringActor instead + * of passing the value directly over the protocol. + * + * @param str String + * The string we are checking the length of. + */ +function stringIsLong(str) { + return str.length >= DebuggerServer.LONG_STRING_LENGTH; +} + +/** + * Create a grip for the given string. + * + * @param str String + * The string we are creating a grip for. + * @param pool ActorPool + * The actor pool where the new actor will be added. + */ +function longStringGrip(str, pool) { + if (!pool.longStringActors) { + pool.longStringActors = {}; + } + + if (pool.longStringActors.hasOwnProperty(str)) { + return pool.longStringActors[str].grip(); + } + + let actor = new LongStringActor(str); + pool.addActor(actor); + pool.longStringActors[str] = actor; + return actor.grip(); +} + +exports.ObjectActor = ObjectActor; +exports.PropertyIteratorActor = PropertyIteratorActor; +exports.LongStringActor = LongStringActor; +exports.createValueGrip = createValueGrip; +exports.stringIsLong = stringIsLong; +exports.longStringGrip = longStringGrip; diff --git a/toolkit/devtools/server/actors/promises.js b/toolkit/devtools/server/actors/promises.js index b3a04285b5..02352d39e2 100644 --- a/toolkit/devtools/server/actors/promises.js +++ b/toolkit/devtools/server/actors/promises.js @@ -7,10 +7,13 @@ const protocol = require("devtools/server/protocol"); const { method, RetVal, Arg, types } = protocol; const { expectState, ActorPool } = require("devtools/server/actors/common"); -const { ObjectActor, createValueGrip } = require("devtools/server/actors/object"); +const { ObjectActor, + createValueGrip } = require("devtools/server/actors/object"); const DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); loader.lazyRequireGetter(this, "events", "sdk/event/core"); +/* global events */ + // Teach protocol.js how to deal with legacy actor types types.addType("ObjectActor", { write: actor => actor.grip(), @@ -86,6 +89,14 @@ let PromisesActor = protocol.ActorClass({ this._newPromises = []; this._promisesSettled = []; + this.dbg.findScripts().forEach(s => { + this.parent.sources.createSourceActors(s.source); + }); + + this.dbg.onNewScript = s => { + this.parent.sources.createSourceActors(s.source); + }; + events.on(this.parent, "window-ready", this._onWindowReady); this.state = "attached"; @@ -142,10 +153,11 @@ let PromisesActor = protocol.ActorClass({ decrementGripDepth: () => this._gripDepth--, createValueGrip: v => createValueGrip(v, this._navigationLifetimePool, this.objectGrip), - sources: () => DevToolsUtils.reportException("PromisesActor", - Error("sources not yet implemented")), + sources: () => this.parent.sources, createEnvironmentActor: () => DevToolsUtils.reportException( - "PromisesActor", Error("createEnvironmentActor not yet implemented")) + "PromisesActor", Error("createEnvironmentActor not yet implemented")), + getGlobalDebugObject: () => DevToolsUtils.reportException( + "PromisesActor", Error("getGlobalDebugObject not yet implemented")), }); this._navigationLifetimePool.addActor(actor); @@ -198,7 +210,7 @@ let PromisesActor = protocol.ActorClass({ */ _makePromiseEventHandler: function(array, eventName) { return promise => { - let actor = this._createObjectActorForPromise(promise) + let actor = this._createObjectActorForPromise(promise); let needsScheduling = array.length == 0; array.push(actor); @@ -208,7 +220,7 @@ let PromisesActor = protocol.ActorClass({ events.emit(this, eventName, array.splice(0, array.length)); }); } - } + }; }, _onWindowReady: expectState("attached", function({ isTopLevel }) { diff --git a/toolkit/devtools/server/actors/script.js b/toolkit/devtools/server/actors/script.js index 405f0a24d1..3370d3fe09 100644 --- a/toolkit/devtools/server/actors/script.js +++ b/toolkit/devtools/server/actors/script.js @@ -9,12 +9,12 @@ const Services = require("Services"); const { Cc, Ci, Cu, components, ChromeWorker } = require("chrome"); const { ActorPool, OriginalLocation, GeneratedLocation } = require("devtools/server/actors/common"); +const { ObjectActor, createValueGrip, longStringGrip } = require("devtools/server/actors/object"); const { DebuggerServer } = require("devtools/server/main"); const DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); const { dbg_assert, dumpn, update, fetch } = DevToolsUtils; const { dirname, joinURI } = require("devtools/toolkit/path"); const promise = require("promise"); -const PromiseDebugging = require("PromiseDebugging"); const xpcInspector = require("xpcInspector"); const ScriptStore = require("./utils/ScriptStore"); const {DevToolsWorker} = require("devtools/toolkit/shared/worker.js"); @@ -32,41 +32,6 @@ loader.lazyRequireGetter(this, "CssLogic", "devtools/styleinspector/css-logic", loader.lazyRequireGetter(this, "events", "sdk/event/core"); loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id"); -let TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array", - "Uint32Array", "Int8Array", "Int16Array", "Int32Array", "Float32Array", - "Float64Array"]; - -// Number of items to preview in objects, arrays, maps, sets, lists, -// collections, etc. -let OBJECT_PREVIEW_MAX_ITEMS = 10; - -/** - * Call PromiseDebugging.getState on this Debugger.Object's referent and wrap - * the resulting `value` or `reason` properties in a Debugger.Object instance. - * - * See dom/webidl/PromiseDebugging.webidl - * - * @returns Object - * An object of one of the following forms: - * - { state: "pending" } - * - { state: "fulfilled", value } - * - { state: "rejected", reason } - */ -function getPromiseState(obj) { - if (obj.class != "Promise") { - throw new Error( - "Can't call `getPromiseState` on `Debugger.Object`s that don't " + - "refer to Promise objects."); - } - - const state = PromiseDebugging.getState(obj.unsafeDereference()); - return { - state: state.state, - value: obj.makeDebuggeeValue(state.value), - reason: obj.makeDebuggeeValue(state.reason) - }; -}; - /** * A BreakpointActorMap is a map from locations to instances of BreakpointActor. */ @@ -467,6 +432,8 @@ function ThreadActor(aParent, aGlobal) this.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this); this.onDebuggerStatement = this.onDebuggerStatement.bind(this); this.onNewScript = this.onNewScript.bind(this); + this.objectGrip = this.objectGrip.bind(this); + this.pauseObjectGrip = this.pauseObjectGrip.bind(this); this._onWindowReady = this._onWindowReady.bind(this); events.on(this._parent, "window-ready", this._onWindowReady); // Set a wrappedJSObject property so |this| can be sent via the observer svc @@ -494,6 +461,9 @@ ThreadActor.prototype = { }, get globalDebugObject() { + if (!this._parent.window) { + return null; + } return this.dbg.makeGlobalObjectReference(this._parent.window); }, @@ -897,7 +867,8 @@ ThreadActor.prototype = { pauseAndRespond: (aFrame, onPacket=k=>k) => { return this._pauseAndRespond(aFrame, { type: "resumeLimit" }, onPacket); }, - createValueGrip: this.createValueGrip.bind(this), + createValueGrip: v => createValueGrip(v, this._pausePool, + this.objectGrip), thread: this, startFrame: this.youngestFrame, startLocation: aStartLocation, @@ -1443,7 +1414,7 @@ ThreadActor.prototype = { let nodeDO = this.globalDebugObject.makeDebuggeeValue(node); listenerForm.node = { selector: selector, - object: this.createValueGrip(nodeDO) + object: createValueGrip(nodeDO, this._pausePool, this.objectGrip) }; listenerForm.type = handler.type; listenerForm.capturing = handler.capturing; @@ -1480,7 +1451,8 @@ ThreadActor.prototype = { while (listenerDO.isBoundFunction) { listenerDO = listenerDO.boundTargetFunction; } - listenerForm.function = this.createValueGrip(listenerDO); + listenerForm.function = createValueGrip(listenerDO, this._pausePool, + this.objectGrip); listeners.push(listenerForm); } } @@ -1655,74 +1627,6 @@ ThreadActor.prototype = { return actor; }, - /** - * Create a grip for the given debuggee value. If the value is an - * object, will create an actor with the given lifetime. - */ - createValueGrip: function (aValue, aPool=false) { - if (!aPool) { - aPool = this._pausePool; - } - - switch (typeof aValue) { - case "boolean": - return aValue; - - case "string": - if (this._stringIsLong(aValue)) { - return this.longStringGrip(aValue, aPool); - } - return aValue; - - case "number": - if (aValue === Infinity) { - return { type: "Infinity" }; - } else if (aValue === -Infinity) { - return { type: "-Infinity" }; - } else if (Number.isNaN(aValue)) { - return { type: "NaN" }; - } else if (!aValue && 1 / aValue === -Infinity) { - return { type: "-0" }; - } - return aValue; - - case "undefined": - return { type: "undefined" }; - - case "object": - if (aValue === null) { - return { type: "null" }; - } - else if(aValue.optimizedOut || - aValue.uninitialized || - aValue.missingArguments) { - // The slot is optimized out, an uninitialized binding, or - // arguments on a dead scope - return { - type: "null", - optimizedOut: aValue.optimizedOut, - uninitialized: aValue.uninitialized, - missingArguments: aValue.missingArguments - }; - } - return this.objectGrip(aValue, aPool); - - case "symbol": - let form = { - type: "symbol" - }; - let name = getSymbolName(aValue); - if (name !== undefined) { - form.name = this.createValueGrip(name); - } - return form; - - default: - dbg_assert(false, "Failed to provide a grip for: " + aValue); - return null; - } - }, - /** * Return a protocol completion value representing the given * Debugger-provided completion value. @@ -1732,11 +1636,14 @@ ThreadActor.prototype = { if (aCompletion == null) { protoValue.terminated = true; } else if ("return" in aCompletion) { - protoValue.return = this.createValueGrip(aCompletion.return); + protoValue.return = createValueGrip(aCompletion.return, + this._pausePool, this.objectGrip); } else if ("throw" in aCompletion) { - protoValue.throw = this.createValueGrip(aCompletion.throw); + protoValue.throw = createValueGrip(aCompletion.throw, + this._pausePool, this.objectGrip); } else { - protoValue.return = this.createValueGrip(aCompletion.yield); + protoValue.return = createValueGrip(aCompletion.yield, + this._pausePool, this.objectGrip); } return protoValue; }, @@ -1760,7 +1667,20 @@ ThreadActor.prototype = { return this.threadLifetimePool.objectActors.get(aValue).grip(); } - let actor = new PauseScopedObjectActor(aValue, this); + let actor = new PauseScopedObjectActor(aValue, { + getGripDepth: () => this._gripDepth, + incrementGripDepth: () => this._gripDepth++, + decrementGripDepth: () => this._gripDepth--, + createValueGrip: v => createValueGrip(v, this._pausePool, + this.pauseObjectGrip), + sources: () => this.sources, + createEnvironmentActor: (env, pool) => + this.createEnvironmentActor(env, pool), + promote: () => this.threadObjectGrip(actor), + isThreadLifetimePool: () => + actor.registeredPool !== this.threadLifetimePool, + getGlobalDebugObject: () => this.globalDebugObject + }); aPool.addActor(actor); aPool.objectActors.set(aValue, actor); return actor.grip(); @@ -1820,29 +1740,6 @@ ThreadActor.prototype = { return {}; }, - /** - * Create a grip for the given string. - * - * @param aString String - * The string we are creating a grip for. - * @param aPool ActorPool - * The actor pool where the new actor will be added. - */ - longStringGrip: function (aString, aPool) { - if (!aPool.longStringActors) { - aPool.longStringActors = {}; - } - - if (aPool.longStringActors.hasOwnProperty(aString)) { - return aPool.longStringActors[aString].grip(); - } - - let actor = new LongStringActor(aString, this); - aPool.addActor(actor); - aPool.longStringActors[aString] = actor; - return actor.grip(); - }, - /** * Create a long string grip that is scoped to a pause. * @@ -1850,7 +1747,7 @@ ThreadActor.prototype = { * The string we are creating a grip for. */ pauseLongStringGrip: function (aString) { - return this.longStringGrip(aString, this._pausePool); + return longStringGrip(aString, this._pausePool); }, /** @@ -1860,18 +1757,7 @@ ThreadActor.prototype = { * The string we are creating a grip for. */ threadLongStringGrip: function (aString) { - return this.longStringGrip(aString, this._threadLifetimePool); - }, - - /** - * Returns true if the string is long enough to use a LongStringActor instead - * of passing the value directly over the protocol. - * - * @param aString String - * The string we are checking the length of. - */ - _stringIsLong: function (aString) { - return aString.length >= DebuggerServer.LONG_STRING_LENGTH; + return longStringGrip(aString, this._threadLifetimePool); }, // JS Debugger API hooks. @@ -1946,7 +1832,9 @@ ThreadActor.prototype = { } packet.why = { type: "exception", - exception: this.createValueGrip(aValue) }; + exception: createValueGrip(aValue, this._pausePool, + this.objectGrip) + }; this.conn.send(packet); this._pushThreadPause(); @@ -2010,9 +1898,22 @@ ThreadActor.prototype = { return false; } + let sourceActor = this.sources.createNonSourceMappedActor(aSource); + + // Go ahead and establish the source actors for this script, which + // fetches sourcemaps if available and sends onNewSource + // notifications. + // + // We need to use synchronize here because if the page is being reloaded, + // this call will replace the previous set of source actors for this source + // with a new one. If the source actors have not been replaced by the time + // we try to reset the breakpoints below, their location objects will still + // point to the old set of source actors, which point to different scripts. + this.synchronize(this.sources.createSourceActors(aSource)); + // Set any stored breakpoints. let promises = []; - let sourceActor = this.sources.createNonSourceMappedActor(aSource); + for (let _actor of this.breakpointActorMap.findActors()) { // XXX bug 1142115: We do async work in here, so we need to // create a fresh binding because for/of does not yet do that in @@ -2022,12 +1923,13 @@ ThreadActor.prototype = { if (actor.isPending) { promises.push(actor.originalLocation.originalSourceActor._setBreakpoint(actor)); } else { - promises.push(this.sources.getGeneratedLocation(actor.originalLocation) - .then((generatedLocation) => { - if (generatedLocation.generatedSourceActor.actorID === sourceActor.actorID) { - sourceActor._setBreakpointAtGeneratedLocation( + promises.push(this.sources.getAllGeneratedLocations(actor.originalLocation) + .then((generatedLocations) => { + if (generatedLocations.length > 0 && + generatedLocations[0].generatedSourceActor.actorID === sourceActor.actorID) { + sourceActor._setBreakpointAtAllGeneratedLocations( actor, - generatedLocation + generatedLocations ); } })); @@ -2038,11 +1940,6 @@ ThreadActor.prototype = { this.synchronize(Promise.all(promises)); } - // Go ahead and establish the source actors for this script, which - // fetches sourcemaps if available and sends onNewSource - // notifications - this.sources.createSourceActors(aSource); - return true; }, @@ -2489,8 +2386,8 @@ SourceActor.prototype = { .then(({ content, contentType }) => { return { from: this.actorID, - source: this.threadActor.createValueGrip( - content, this.threadActor.threadLifetimePool), + source: createValueGrip(content, this.threadActor.threadLifetimePool, + this.threadActor.objectGrip), contentType: contentType }; }) @@ -2498,7 +2395,7 @@ SourceActor.prototype = { reportError(aError, "Got an exception during SA_onSource: "); return { "from": this.actorID, - "error": "loadSourceError", + "error": this.url, "message": "Could not load the source for " + this.url + ".\n" + DevToolsUtils.safeErrorString(aError) }; @@ -3035,7 +2932,9 @@ SourceActor.prototype = { let scripts = this.scripts.getScriptsBySourceActorAndLine( generatedSourceActor, generatedLine - ).filter((script) => !actor.hasScript(script)); + ); + + scripts = scripts.filter((script) => !actor.hasScript(script)); // Find all entry points that correspond to the given location. let entryPoints = []; @@ -3084,1318 +2983,14 @@ SourceActor.prototype.requestTypes = { exports.SourceActor = SourceActor; -/** - * Determine if a given value is non-primitive. - * - * @param Any aValue - * The value to test. - * @return Boolean - * Whether the value is non-primitive. - */ -function isObject(aValue) { - const type = typeof aValue; - return type == "object" ? aValue !== null : type == "function"; -} - -/** - * Create a function that can safely stringify Debugger.Objects of a given - * builtin type. - * - * @param Function aCtor - * The builtin class constructor. - * @return Function - * The stringifier for the class. - */ -function createBuiltinStringifier(aCtor) { - return aObj => aCtor.prototype.toString.call(aObj.unsafeDereference()); -} - -/** - * Stringify a Debugger.Object-wrapped Error instance. - * - * @param Debugger.Object aObj - * The object to stringify. - * @return String - * The stringification of the object. - */ -function errorStringify(aObj) { - let name = DevToolsUtils.getProperty(aObj, "name"); - if (name === "" || name === undefined) { - name = aObj.class; - } else if (isObject(name)) { - name = stringify(name); - } - - let message = DevToolsUtils.getProperty(aObj, "message"); - if (isObject(message)) { - message = stringify(message); - } - - if (message === "" || message === undefined) { - return name; - } - return name + ": " + message; -} - -/** - * Stringify a Debugger.Object based on its class. - * - * @param Debugger.Object aObj - * The object to stringify. - * @return String - * The stringification for the object. - */ -function stringify(aObj) { - if (aObj.class == "DeadObject") { - const error = new Error("Dead object encountered."); - DevToolsUtils.reportException("stringify", error); - return ""; - } - - const stringifier = stringifiers[aObj.class] || stringifiers.Object; - - try { - return stringifier(aObj); - } catch (e) { - DevToolsUtils.reportException("stringify", e); - return ""; - } -} - -// Used to prevent infinite recursion when an array is found inside itself. -let seen = null; - -let stringifiers = { - Error: errorStringify, - EvalError: errorStringify, - RangeError: errorStringify, - ReferenceError: errorStringify, - SyntaxError: errorStringify, - TypeError: errorStringify, - URIError: errorStringify, - Boolean: createBuiltinStringifier(Boolean), - Function: createBuiltinStringifier(Function), - Number: createBuiltinStringifier(Number), - RegExp: createBuiltinStringifier(RegExp), - String: createBuiltinStringifier(String), - Object: obj => "[object " + obj.class + "]", - Array: obj => { - // If we're at the top level then we need to create the Set for tracking - // previously stringified arrays. - const topLevel = !seen; - if (topLevel) { - seen = new Set(); - } else if (seen.has(obj)) { - return ""; - } - - seen.add(obj); - - const len = DevToolsUtils.getProperty(obj, "length"); - let string = ""; - - // The following check is only required because the debuggee could possibly - // be a Proxy and return any value. For normal objects, array.length is - // always a non-negative integer. - if (typeof len == "number" && len > 0) { - for (let i = 0; i < len; i++) { - const desc = obj.getOwnPropertyDescriptor(i); - if (desc) { - const { value } = desc; - if (value != null) { - string += isObject(value) ? stringify(value) : value; - } - } - - if (i < len - 1) { - string += ","; - } - } - } - - if (topLevel) { - seen = null; - } - - return string; - }, - DOMException: obj => { - const message = DevToolsUtils.getProperty(obj, "message") || ""; - const result = (+DevToolsUtils.getProperty(obj, "result")).toString(16); - const code = DevToolsUtils.getProperty(obj, "code"); - const name = DevToolsUtils.getProperty(obj, "name") || ""; - - return '[Exception... "' + message + '" ' + - 'code: "' + code +'" ' + - 'nsresult: "0x' + result + ' (' + name + ')"]'; - }, - Promise: obj => { - const { state, value, reason } = getPromiseState(obj); - let statePreview = state; - if (state != "pending") { - const settledValue = state === "fulfilled" ? value : reason; - statePreview += ": " + (typeof settledValue === "object" && settledValue !== null - ? stringify(settledValue) - : settledValue); - } - return "Promise (" + statePreview + ")"; - }, -}; - -/** - * Creates an actor for the specified object. - * - * @param aObj Debugger.Object - * The debuggee object. - * @param aThreadActor ThreadActor - * The parent thread actor for this object. - */ -function ObjectActor(aObj, aThreadActor) -{ - dbg_assert(!aObj.optimizedOut, "Should not create object actors for optimized out values!"); - this.obj = aObj; - this.threadActor = aThreadActor; -} - -ObjectActor.prototype = { - actorPrefix: "obj", - - /** - * Returns a grip for this actor for returning in a protocol message. - */ - grip: function () { - this.threadActor._gripDepth++; - - let g = { - "type": "object", - "class": this.obj.class, - "actor": this.actorID, - "extensible": this.obj.isExtensible(), - "frozen": this.obj.isFrozen(), - "sealed": this.obj.isSealed() - }; - - if (this.obj.class != "DeadObject") { - // Expose internal Promise state. - if (this.obj.class == "Promise") { - const { state, value, reason } = getPromiseState(this.obj); - g.promiseState = { state }; - if (state == "fulfilled") { - g.promiseState.value = this.threadActor.createValueGrip(value); - } else if (state == "rejected") { - g.promiseState.reason = this.threadActor.createValueGrip(reason); - } - } - - let raw = this.obj.unsafeDereference(); - - // If Cu is not defined, we are running on a worker thread, where xrays - // don't exist. - if (Cu) { - raw = Cu.unwaiveXrays(raw); - } - - if (!DevToolsUtils.isSafeJSObject(raw)) { - raw = null; - } - - let previewers = DebuggerServer.ObjectActorPreviewers[this.obj.class] || - DebuggerServer.ObjectActorPreviewers.Object; - for (let fn of previewers) { - try { - if (fn(this, g, raw)) { - break; - } - } catch (e) { - DevToolsUtils.reportException("ObjectActor.prototype.grip previewer function", e); - } - } - } - - this.threadActor._gripDepth--; - return g; - }, - - /** - * Releases this actor from the pool. - */ - release: function () { - if (this.registeredPool.objectActors) { - this.registeredPool.objectActors.delete(this.obj); - } - this.registeredPool.removeActor(this); - }, - - /** - * Handle a protocol request to provide the definition site of this function - * object. - * - * @param aRequest object - * The protocol request object. - */ - onDefinitionSite: function OA_onDefinitionSite(aRequest) { - if (this.obj.class != "Function") { - return { - from: this.actorID, - error: "objectNotFunction", - message: this.actorID + " is not a function." - }; - } - - if (!this.obj.script) { - return { - from: this.actorID, - error: "noScript", - message: this.actorID + " has no Debugger.Script" - }; - } - - return this.threadActor.sources.getOriginalLocation(new GeneratedLocation( - this.threadActor.sources.createNonSourceMappedActor(this.obj.script.source), - this.obj.script.startLine, - 0 // TODO bug 901138: use Debugger.Script.prototype.startColumn - )).then((originalLocation) => { - return { - source: originalLocation.originalSourceActor.form(), - line: originalLocation.originalLine, - column: originalLocation.originalColumn - }; - }); - }, - - /** - * Handle a protocol request to provide the names of the properties defined on - * the object and not its prototype. - * - * @param aRequest object - * The protocol request object. - */ - onOwnPropertyNames: function (aRequest) { - return { from: this.actorID, - ownPropertyNames: this.obj.getOwnPropertyNames() }; - }, - - /** - * Handle a protocol request to provide the prototype and own properties of - * the object. - * - * @param aRequest object - * The protocol request object. - */ - onPrototypeAndProperties: function (aRequest) { - let ownProperties = Object.create(null); - let names; - try { - names = this.obj.getOwnPropertyNames(); - } catch (ex) { - // The above can throw if this.obj points to a dead object. - // TODO: we should use Cu.isDeadWrapper() - see bug 885800. - return { from: this.actorID, - prototype: this.threadActor.createValueGrip(null), - ownProperties: ownProperties, - safeGetterValues: Object.create(null) }; - } - for (let name of names) { - ownProperties[name] = this._propertyDescriptor(name); - } - return { from: this.actorID, - prototype: this.threadActor.createValueGrip(this.obj.proto), - ownProperties: ownProperties, - safeGetterValues: this._findSafeGetterValues(ownProperties) }; - }, - - /** - * Find the safe getter values for the current Debugger.Object, |this.obj|. - * - * @private - * @param object aOwnProperties - * The object that holds the list of known ownProperties for - * |this.obj|. - * @param number [aLimit=0] - * Optional limit of getter values to find. - * @return object - * An object that maps property names to safe getter descriptors as - * defined by the remote debugging protocol. - */ - _findSafeGetterValues: function (aOwnProperties, aLimit = 0) - { - let safeGetterValues = Object.create(null); - let obj = this.obj; - let level = 0, i = 0; - - while (obj) { - let getters = this._findSafeGetters(obj); - for (let name of getters) { - // Avoid overwriting properties from prototypes closer to this.obj. Also - // avoid providing safeGetterValues from prototypes if property |name| - // is already defined as an own property. - if (name in safeGetterValues || - (obj != this.obj && name in aOwnProperties)) { - continue; - } - - // Ignore __proto__ on Object.prototye. - if (!obj.proto && name == "__proto__") { - continue; - } - - let desc = null, getter = null; - try { - desc = obj.getOwnPropertyDescriptor(name); - getter = desc.get; - } catch (ex) { - // The above can throw if the cache becomes stale. - } - if (!getter) { - obj._safeGetters = null; - continue; - } - - let result = getter.call(this.obj); - if (result && !("throw" in result)) { - let getterValue = undefined; - if ("return" in result) { - getterValue = result.return; - } else if ("yield" in result) { - getterValue = result.yield; - } - // WebIDL attributes specified with the LenientThis extended attribute - // return undefined and should be ignored. - if (getterValue !== undefined) { - safeGetterValues[name] = { - getterValue: this.threadActor.createValueGrip(getterValue), - getterPrototypeLevel: level, - enumerable: desc.enumerable, - writable: level == 0 ? desc.writable : true, - }; - if (aLimit && ++i == aLimit) { - break; - } - } - } - } - if (aLimit && i == aLimit) { - break; - } - - obj = obj.proto; - level++; - } - - return safeGetterValues; - }, - - /** - * Find the safe getters for a given Debugger.Object. Safe getters are native - * getters which are safe to execute. - * - * @private - * @param Debugger.Object aObject - * The Debugger.Object where you want to find safe getters. - * @return Set - * A Set of names of safe getters. This result is cached for each - * Debugger.Object. - */ - _findSafeGetters: function (aObject) - { - if (aObject._safeGetters) { - return aObject._safeGetters; - } - - let getters = new Set(); - let names = []; - try { - names = aObject.getOwnPropertyNames() - } catch (ex) { - // Calling getOwnPropertyNames() on some wrapped native prototypes is not - // allowed: "cannot modify properties of a WrappedNative". See bug 952093. - } - - for (let name of names) { - let desc = null; - try { - desc = aObject.getOwnPropertyDescriptor(name); - } catch (e) { - // Calling getOwnPropertyDescriptor on wrapped native prototypes is not - // allowed (bug 560072). - } - if (!desc || desc.value !== undefined || !("get" in desc)) { - continue; - } - - if (DevToolsUtils.hasSafeGetter(desc)) { - getters.add(name); - } - } - - aObject._safeGetters = getters; - return getters; - }, - - /** - * Handle a protocol request to provide the prototype of the object. - * - * @param aRequest object - * The protocol request object. - */ - onPrototype: function (aRequest) { - return { from: this.actorID, - prototype: this.threadActor.createValueGrip(this.obj.proto) }; - }, - - /** - * Handle a protocol request to provide the property descriptor of the - * object's specified property. - * - * @param aRequest object - * The protocol request object. - */ - onProperty: function (aRequest) { - if (!aRequest.name) { - return { error: "missingParameter", - message: "no property name was specified" }; - } - - return { from: this.actorID, - descriptor: this._propertyDescriptor(aRequest.name) }; - }, - - /** - * Handle a protocol request to provide the display string for the object. - * - * @param aRequest object - * The protocol request object. - */ - onDisplayString: function (aRequest) { - const string = stringify(this.obj); - return { from: this.actorID, - displayString: this.threadActor.createValueGrip(string) }; - }, - - /** - * A helper method that creates a property descriptor for the provided object, - * properly formatted for sending in a protocol response. - * - * @private - * @param string aName - * The property that the descriptor is generated for. - * @param boolean [aOnlyEnumerable] - * Optional: true if you want a descriptor only for an enumerable - * property, false otherwise. - * @return object|undefined - * The property descriptor, or undefined if this is not an enumerable - * property and aOnlyEnumerable=true. - */ - _propertyDescriptor: function (aName, aOnlyEnumerable) { - let desc; - try { - desc = this.obj.getOwnPropertyDescriptor(aName); - } catch (e) { - // Calling getOwnPropertyDescriptor on wrapped native prototypes is not - // allowed (bug 560072). Inform the user with a bogus, but hopefully - // explanatory, descriptor. - return { - configurable: false, - writable: false, - enumerable: false, - value: e.name - }; - } - - if (!desc || aOnlyEnumerable && !desc.enumerable) { - return undefined; - } - - let retval = { - configurable: desc.configurable, - enumerable: desc.enumerable - }; - - if ("value" in desc) { - retval.writable = desc.writable; - retval.value = this.threadActor.createValueGrip(desc.value); - } else { - if ("get" in desc) { - retval.get = this.threadActor.createValueGrip(desc.get); - } - if ("set" in desc) { - retval.set = this.threadActor.createValueGrip(desc.set); - } - } - return retval; - }, - - /** - * Handle a protocol request to provide the source code of a function. - * - * @param aRequest object - * The protocol request object. - */ - onDecompile: function (aRequest) { - if (this.obj.class !== "Function") { - return { error: "objectNotFunction", - message: "decompile request is only valid for object grips " + - "with a 'Function' class." }; - } - - return { from: this.actorID, - decompiledCode: this.obj.decompile(!!aRequest.pretty) }; - }, - - /** - * Handle a protocol request to provide the parameters of a function. - * - * @param aRequest object - * The protocol request object. - */ - onParameterNames: function (aRequest) { - if (this.obj.class !== "Function") { - return { error: "objectNotFunction", - message: "'parameterNames' request is only valid for object " + - "grips with a 'Function' class." }; - } - - return { parameterNames: this.obj.parameterNames }; - }, - - /** - * Handle a protocol request to release a thread-lifetime grip. - * - * @param aRequest object - * The protocol request object. - */ - onRelease: function (aRequest) { - this.release(); - return {}; - }, - - /** - * Handle a protocol request to provide the lexical scope of a function. - * - * @param aRequest object - * The protocol request object. - */ - onScope: function (aRequest) { - if (this.obj.class !== "Function") { - return { error: "objectNotFunction", - message: "scope request is only valid for object grips with a" + - " 'Function' class." }; - } - - let envActor = this.threadActor.createEnvironmentActor(this.obj.environment, - this.registeredPool); - if (!envActor) { - return { error: "notDebuggee", - message: "cannot access the environment of this function." }; - } - - return { from: this.actorID, scope: envActor.form() }; - } -}; - -ObjectActor.prototype.requestTypes = { - "definitionSite": ObjectActor.prototype.onDefinitionSite, - "parameterNames": ObjectActor.prototype.onParameterNames, - "prototypeAndProperties": ObjectActor.prototype.onPrototypeAndProperties, - "prototype": ObjectActor.prototype.onPrototype, - "property": ObjectActor.prototype.onProperty, - "displayString": ObjectActor.prototype.onDisplayString, - "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames, - "decompile": ObjectActor.prototype.onDecompile, - "release": ObjectActor.prototype.onRelease, - "scope": ObjectActor.prototype.onScope, -}; - -exports.ObjectActor = ObjectActor; - -/** - * Functions for adding information to ObjectActor grips for the purpose of - * having customized output. This object holds arrays mapped by - * Debugger.Object.prototype.class. - * - * In each array you can add functions that take two - * arguments: - * - the ObjectActor instance to make a preview for, - * - the grip object being prepared for the client, - * - the raw JS object after calling Debugger.Object.unsafeDereference(). This - * argument is only provided if the object is safe for reading properties and - * executing methods. See DevToolsUtils.isSafeJSObject(). - * - * Functions must return false if they cannot provide preview - * information for the debugger object, or true otherwise. - */ -DebuggerServer.ObjectActorPreviewers = { - String: [function({obj, threadActor}, aGrip) { - let result = genericObjectPreviewer("String", String, obj, threadActor); - let length = DevToolsUtils.getProperty(obj, "length"); - - if (!result || typeof length != "number") { - return false; - } - - aGrip.preview = { - kind: "ArrayLike", - length: length - }; - - if (threadActor._gripDepth > 1) { - return true; - } - - let items = aGrip.preview.items = []; - - const max = Math.min(result.value.length, OBJECT_PREVIEW_MAX_ITEMS); - for (let i = 0; i < max; i++) { - let value = threadActor.createValueGrip(result.value[i]); - items.push(value); - } - - return true; - }], - - Boolean: [function({obj, threadActor}, aGrip) { - let result = genericObjectPreviewer("Boolean", Boolean, obj, threadActor); - if (result) { - aGrip.preview = result; - return true; - } - - return false; - }], - - Number: [function({obj, threadActor}, aGrip) { - let result = genericObjectPreviewer("Number", Number, obj, threadActor); - if (result) { - aGrip.preview = result; - return true; - } - - return false; - }], - - Function: [function({obj, threadActor}, aGrip) { - if (obj.name) { - aGrip.name = obj.name; - } - - if (obj.displayName) { - aGrip.displayName = obj.displayName.substr(0, 500); - } - - if (obj.parameterNames) { - aGrip.parameterNames = obj.parameterNames; - } - - // Check if the developer has added a de-facto standard displayName - // property for us to use. - let userDisplayName; - try { - userDisplayName = obj.getOwnPropertyDescriptor("displayName"); - } catch (e) { - // Calling getOwnPropertyDescriptor with displayName might throw - // with "permission denied" errors for some functions. - dumpn(e); - } - - if (userDisplayName && typeof userDisplayName.value == "string" && - userDisplayName.value) { - aGrip.userDisplayName = threadActor.createValueGrip(userDisplayName.value); - } - - return true; - }], - - RegExp: [function({obj, threadActor}, aGrip) { - // Avoid having any special preview for the RegExp.prototype itself. - if (!obj.proto || obj.proto.class != "RegExp") { - return false; - } - - let str = RegExp.prototype.toString.call(obj.unsafeDereference()); - aGrip.displayString = threadActor.createValueGrip(str); - return true; - }], - - Date: [function({obj, threadActor}, aGrip) { - let time = Date.prototype.getTime.call(obj.unsafeDereference()); - - aGrip.preview = { - timestamp: threadActor.createValueGrip(time), - }; - return true; - }], - - Array: [function({obj, threadActor}, aGrip) { - let length = DevToolsUtils.getProperty(obj, "length"); - if (typeof length != "number") { - return false; - } - - aGrip.preview = { - kind: "ArrayLike", - length: length, - }; - - if (threadActor._gripDepth > 1) { - return true; - } - - let raw = obj.unsafeDereference(); - let items = aGrip.preview.items = []; - - for (let i = 0; i < length; ++i) { - // Array Xrays filter out various possibly-unsafe properties (like - // functions, and claim that the value is undefined instead. This - // is generally the right thing for privileged code accessing untrusted - // objects, but quite confusing for Object previews. So we manually - // override this protection by waiving Xrays on the array, and re-applying - // Xrays on any indexed value props that we pull off of it. - let desc = Object.getOwnPropertyDescriptor(Cu.waiveXrays(raw), i); - if (desc && !desc.get && !desc.set) { - let value = Cu.unwaiveXrays(desc.value); - value = makeDebuggeeValueIfNeeded(obj, value); - items.push(threadActor.createValueGrip(value)); - } else { - items.push(null); - } - - if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { - break; - } - } - - return true; - }], // Array - - Set: [function({obj, threadActor}, aGrip) { - let size = DevToolsUtils.getProperty(obj, "size"); - if (typeof size != "number") { - return false; - } - - aGrip.preview = { - kind: "ArrayLike", - length: size, - }; - - // Avoid recursive object grips. - if (threadActor._gripDepth > 1) { - return true; - } - - let raw = obj.unsafeDereference(); - let items = aGrip.preview.items = []; - // We currently lack XrayWrappers for Set, so when we iterate over - // the values, the temporary iterator objects get created in the target - // compartment. However, we _do_ have Xrays to Object now, so we end up - // Xraying those temporary objects, and filtering access to |it.value| - // based on whether or not it's Xrayable and/or callable, which breaks - // the for/of iteration. - // - // This code is designed to handle untrusted objects, so we can safely - // waive Xrays on the iterable, and relying on the Debugger machinery to - // make sure we handle the resulting objects carefully. - for (let item of Cu.waiveXrays(Set.prototype.values.call(raw))) { - item = Cu.unwaiveXrays(item); - item = makeDebuggeeValueIfNeeded(obj, item); - items.push(threadActor.createValueGrip(item)); - if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { - break; - } - } - - return true; - }], // Set - - Map: [function({obj, threadActor}, aGrip) { - let size = DevToolsUtils.getProperty(obj, "size"); - if (typeof size != "number") { - return false; - } - - aGrip.preview = { - kind: "MapLike", - size: size, - }; - - if (threadActor._gripDepth > 1) { - return true; - } - - let raw = obj.unsafeDereference(); - let entries = aGrip.preview.entries = []; - // Iterating over a Map via .entries goes through various intermediate - // objects - an Iterator object, then a 2-element Array object, then the - // actual values we care about. We don't have Xrays to Iterator objects, - // so we get Opaque wrappers for them. And even though we have Xrays to - // Arrays, the semantics often deny access to the entires based on the - // nature of the values. So we need waive Xrays for the iterator object - // and the tupes, and then re-apply them on the underlying values until - // we fix bug 1023984. - // - // Even then though, we might want to continue waiving Xrays here for the - // same reason we do so for Arrays above - this filtering behavior is likely - // to be more confusing than beneficial in the case of Object previews. - for (let keyValuePair of Cu.waiveXrays(Map.prototype.entries.call(raw))) { - let key = Cu.unwaiveXrays(keyValuePair[0]); - let value = Cu.unwaiveXrays(keyValuePair[1]); - key = makeDebuggeeValueIfNeeded(obj, key); - value = makeDebuggeeValueIfNeeded(obj, value); - entries.push([threadActor.createValueGrip(key), - threadActor.createValueGrip(value)]); - if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { - break; - } - } - - return true; - }], // Map - - DOMStringMap: [function({obj, threadActor}, aGrip, aRawObj) { - if (!aRawObj) { - return false; - } - - let keys = obj.getOwnPropertyNames(); - aGrip.preview = { - kind: "MapLike", - size: keys.length, - }; - - if (threadActor._gripDepth > 1) { - return true; - } - - let entries = aGrip.preview.entries = []; - for (let key of keys) { - let value = makeDebuggeeValueIfNeeded(obj, aRawObj[key]); - entries.push([key, threadActor.createValueGrip(value)]); - if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { - break; - } - } - - return true; - }], // DOMStringMap - -}; // DebuggerServer.ObjectActorPreviewers - -/** - * Generic previewer for "simple" classes like String, Number and Boolean. - * - * @param string aClassName - * Class name to expect. - * @param object aClass - * The class to expect, eg. String. The valueOf() method of the class is - * invoked on the given object. - * @param Debugger.Object aObj - * The debugger object we need to preview. - * @param object aThreadActor - * The thread actor to use to create a value grip. - * @return object|null - * An object with one property, "value", which holds the value grip that - * represents the given object. Null is returned if we cant preview the - * object. - */ -function genericObjectPreviewer(aClassName, aClass, aObj, aThreadActor) { - if (!aObj.proto || aObj.proto.class != aClassName) { - return null; - } - - let raw = aObj.unsafeDereference(); - let v = null; - try { - v = aClass.prototype.valueOf.call(raw); - } catch (ex) { - // valueOf() can throw if the raw JS object is "misbehaved". - return null; - } - - if (v !== null) { - v = aThreadActor.createValueGrip(makeDebuggeeValueIfNeeded(aObj, v)); - return { value: v }; - } - - return null; -} - -// Preview functions that do not rely on the object class. -DebuggerServer.ObjectActorPreviewers.Object = [ - function TypedArray({obj, threadActor}, aGrip) { - if (TYPED_ARRAY_CLASSES.indexOf(obj.class) == -1) { - return false; - } - - let length = DevToolsUtils.getProperty(obj, "length"); - if (typeof length != "number") { - return false; - } - - aGrip.preview = { - kind: "ArrayLike", - length: length, - }; - - if (threadActor._gripDepth > 1) { - return true; - } - - let raw = obj.unsafeDereference(); - let global = Cu.getGlobalForObject(DebuggerServer); - let classProto = global[obj.class].prototype; - // The Xray machinery for TypedArrays denies indexed access on the grounds - // that it's slow, and advises callers to do a structured clone instead. - let safeView = Cu.cloneInto(classProto.subarray.call(raw, 0, OBJECT_PREVIEW_MAX_ITEMS), global); - let items = aGrip.preview.items = []; - for (let i = 0; i < safeView.length; i++) { - items.push(safeView[i]); - } - - return true; - }, - - function Error({obj, threadActor}, aGrip) { - switch (obj.class) { - case "Error": - case "EvalError": - case "RangeError": - case "ReferenceError": - case "SyntaxError": - case "TypeError": - case "URIError": - let name = DevToolsUtils.getProperty(obj, "name"); - let msg = DevToolsUtils.getProperty(obj, "message"); - let stack = DevToolsUtils.getProperty(obj, "stack"); - let fileName = DevToolsUtils.getProperty(obj, "fileName"); - let lineNumber = DevToolsUtils.getProperty(obj, "lineNumber"); - let columnNumber = DevToolsUtils.getProperty(obj, "columnNumber"); - aGrip.preview = { - kind: "Error", - name: threadActor.createValueGrip(name), - message: threadActor.createValueGrip(msg), - stack: threadActor.createValueGrip(stack), - fileName: threadActor.createValueGrip(fileName), - lineNumber: threadActor.createValueGrip(lineNumber), - columnNumber: threadActor.createValueGrip(columnNumber), - }; - return true; - default: - return false; - } - }, - - function CSSMediaRule({obj, threadActor}, aGrip, aRawObj) { - if (isWorker || !aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSMediaRule)) { - return false; - } - aGrip.preview = { - kind: "ObjectWithText", - text: threadActor.createValueGrip(aRawObj.conditionText), - }; - return true; - }, - - function CSSStyleRule({obj, threadActor}, aGrip, aRawObj) { - if (isWorker || !aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSStyleRule)) { - return false; - } - aGrip.preview = { - kind: "ObjectWithText", - text: threadActor.createValueGrip(aRawObj.selectorText), - }; - return true; - }, - - function ObjectWithURL({obj, threadActor}, aGrip, aRawObj) { - if (isWorker || !aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSImportRule || - aRawObj instanceof Ci.nsIDOMCSSStyleSheet || - aRawObj instanceof Ci.nsIDOMLocation || - aRawObj instanceof Ci.nsIDOMWindow)) { - return false; - } - - let url; - if (aRawObj instanceof Ci.nsIDOMWindow && aRawObj.location) { - url = aRawObj.location.href; - } else if (aRawObj.href) { - url = aRawObj.href; - } else { - return false; - } - - aGrip.preview = { - kind: "ObjectWithURL", - url: threadActor.createValueGrip(url), - }; - - return true; - }, - - function ArrayLike({obj, threadActor}, aGrip, aRawObj) { - if (isWorker || !aRawObj || - obj.class != "DOMStringList" && - obj.class != "DOMTokenList" && - !(aRawObj instanceof Ci.nsIDOMMozNamedAttrMap || - aRawObj instanceof Ci.nsIDOMCSSRuleList || - aRawObj instanceof Ci.nsIDOMCSSValueList || - aRawObj instanceof Ci.nsIDOMFileList || - aRawObj instanceof Ci.nsIDOMFontFaceList || - aRawObj instanceof Ci.nsIDOMMediaList || - aRawObj instanceof Ci.nsIDOMNodeList || - aRawObj instanceof Ci.nsIDOMStyleSheetList)) { - return false; - } - - if (typeof aRawObj.length != "number") { - return false; - } - - aGrip.preview = { - kind: "ArrayLike", - length: aRawObj.length, - }; - - if (threadActor._gripDepth > 1) { - return true; - } - - let items = aGrip.preview.items = []; - - for (let i = 0; i < aRawObj.length && - items.length < OBJECT_PREVIEW_MAX_ITEMS; i++) { - let value = makeDebuggeeValueIfNeeded(obj, aRawObj[i]); - items.push(threadActor.createValueGrip(value)); - } - - return true; - }, // ArrayLike - - function CSSStyleDeclaration({obj, threadActor}, aGrip, aRawObj) { - if (isWorker || !aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSStyleDeclaration)) { - return false; - } - - aGrip.preview = { - kind: "MapLike", - size: aRawObj.length, - }; - - let entries = aGrip.preview.entries = []; - - for (let i = 0; i < OBJECT_PREVIEW_MAX_ITEMS && - i < aRawObj.length; i++) { - let prop = aRawObj[i]; - let value = aRawObj.getPropertyValue(prop); - entries.push([prop, threadActor.createValueGrip(value)]); - } - - return true; - }, - - function DOMNode({obj, threadActor}, aGrip, aRawObj) { - if (isWorker || obj.class == "Object" || !aRawObj || - !(aRawObj instanceof Ci.nsIDOMNode)) { - return false; - } - - let preview = aGrip.preview = { - kind: "DOMNode", - nodeType: aRawObj.nodeType, - nodeName: aRawObj.nodeName, - }; - - if (aRawObj instanceof Ci.nsIDOMDocument && aRawObj.location) { - preview.location = threadActor.createValueGrip(aRawObj.location.href); - } else if (aRawObj instanceof Ci.nsIDOMDocumentFragment) { - preview.childNodesLength = aRawObj.childNodes.length; - - if (threadActor._gripDepth < 2) { - preview.childNodes = []; - for (let node of aRawObj.childNodes) { - let actor = threadActor.createValueGrip(obj.makeDebuggeeValue(node)); - preview.childNodes.push(actor); - if (preview.childNodes.length == OBJECT_PREVIEW_MAX_ITEMS) { - break; - } - } - } - } else if (aRawObj instanceof Ci.nsIDOMElement) { - // Add preview for DOM element attributes. - if (aRawObj instanceof Ci.nsIDOMHTMLElement) { - preview.nodeName = preview.nodeName.toLowerCase(); - } - - let i = 0; - preview.attributes = {}; - preview.attributesLength = aRawObj.attributes.length; - for (let attr of aRawObj.attributes) { - preview.attributes[attr.nodeName] = threadActor.createValueGrip(attr.value); - if (++i == OBJECT_PREVIEW_MAX_ITEMS) { - break; - } - } - } else if (aRawObj instanceof Ci.nsIDOMAttr) { - preview.value = threadActor.createValueGrip(aRawObj.value); - } else if (aRawObj instanceof Ci.nsIDOMText || - aRawObj instanceof Ci.nsIDOMComment) { - preview.textContent = threadActor.createValueGrip(aRawObj.textContent); - } - - return true; - }, // DOMNode - - function DOMEvent({obj, threadActor}, aGrip, aRawObj) { - if (isWorker || !aRawObj || !(aRawObj instanceof Ci.nsIDOMEvent)) { - return false; - } - - let preview = aGrip.preview = { - kind: "DOMEvent", - type: aRawObj.type, - properties: Object.create(null), - }; - - if (threadActor._gripDepth < 2) { - let target = obj.makeDebuggeeValue(aRawObj.target); - preview.target = threadActor.createValueGrip(target); - } - - let props = []; - if (aRawObj instanceof Ci.nsIDOMMouseEvent) { - props.push("buttons", "clientX", "clientY", "layerX", "layerY"); - } else if (aRawObj instanceof Ci.nsIDOMKeyEvent) { - let modifiers = []; - if (aRawObj.altKey) { - modifiers.push("Alt"); - } - if (aRawObj.ctrlKey) { - modifiers.push("Control"); - } - if (aRawObj.metaKey) { - modifiers.push("Meta"); - } - if (aRawObj.shiftKey) { - modifiers.push("Shift"); - } - preview.eventKind = "key"; - preview.modifiers = modifiers; - - props.push("key", "charCode", "keyCode"); - } else if (aRawObj instanceof Ci.nsIDOMTransitionEvent) { - props.push("propertyName", "pseudoElement"); - } else if (aRawObj instanceof Ci.nsIDOMAnimationEvent) { - props.push("animationName", "pseudoElement"); - } else if (aRawObj instanceof Ci.nsIDOMClipboardEvent) { - props.push("clipboardData"); - } - - // Add event-specific properties. - for (let prop of props) { - let value = aRawObj[prop]; - if (value && (typeof value == "object" || typeof value == "function")) { - // Skip properties pointing to objects. - if (threadActor._gripDepth > 1) { - continue; - } - value = obj.makeDebuggeeValue(value); - } - preview.properties[prop] = threadActor.createValueGrip(value); - } - - // Add any properties we find on the event object. - if (!props.length) { - let i = 0; - for (let prop in aRawObj) { - let value = aRawObj[prop]; - if (prop == "target" || prop == "type" || value === null || - typeof value == "function") { - continue; - } - if (value && typeof value == "object") { - if (threadActor._gripDepth > 1) { - continue; - } - value = obj.makeDebuggeeValue(value); - } - preview.properties[prop] = threadActor.createValueGrip(value); - if (++i == OBJECT_PREVIEW_MAX_ITEMS) { - break; - } - } - } - - return true; - }, // DOMEvent - - function DOMException({obj, threadActor}, aGrip, aRawObj) { - if (isWorker || !aRawObj || !(aRawObj instanceof Ci.nsIDOMDOMException)) { - return false; - } - - aGrip.preview = { - kind: "DOMException", - name: threadActor.createValueGrip(aRawObj.name), - message: threadActor.createValueGrip(aRawObj.message), - code: threadActor.createValueGrip(aRawObj.code), - result: threadActor.createValueGrip(aRawObj.result), - filename: threadActor.createValueGrip(aRawObj.filename), - lineNumber: threadActor.createValueGrip(aRawObj.lineNumber), - columnNumber: threadActor.createValueGrip(aRawObj.columnNumber), - }; - - return true; - }, - - function GenericObject(aObjectActor, aGrip) { - let {obj, threadActor} = aObjectActor; - if (aGrip.preview || aGrip.displayString || threadActor._gripDepth > 1) { - return false; - } - - let i = 0, names = []; - let preview = aGrip.preview = { - kind: "Object", - ownProperties: Object.create(null), - }; - - try { - names = obj.getOwnPropertyNames(); - } catch (ex) { - // Calling getOwnPropertyNames() on some wrapped native prototypes is not - // allowed: "cannot modify properties of a WrappedNative". See bug 952093. - } - - preview.ownPropertiesLength = names.length; - - for (let name of names) { - let desc = aObjectActor._propertyDescriptor(name, true); - if (!desc) { - continue; - } - - preview.ownProperties[name] = desc; - if (++i == OBJECT_PREVIEW_MAX_ITEMS) { - break; - } - } - - if (i < OBJECT_PREVIEW_MAX_ITEMS) { - preview.safeGetterValues = aObjectActor. - _findSafeGetterValues(preview.ownProperties, - OBJECT_PREVIEW_MAX_ITEMS - i); - } - - return true; - }, // GenericObject -]; // DebuggerServer.ObjectActorPreviewers.Object - /** * Creates a pause-scoped actor for the specified object. * @see ObjectActor */ -function PauseScopedObjectActor() -{ - ObjectActor.apply(this, arguments); +function PauseScopedObjectActor(obj, hooks) { + ObjectActor.call(this, obj, hooks); + this.hooks.promote = hooks.promote; + this.hooks.isThreadLifetimePool = hooks.isThreadLifetimePool; } PauseScopedObjectActor.prototype = Object.create(PauseScopedActor.prototype); @@ -4430,7 +3025,7 @@ update(PauseScopedObjectActor.prototype, { * The protocol request object. */ onThreadGrip: PauseScopedActor.withPaused(function (aRequest) { - this.threadActor.threadObjectGrip(this); + this.hooks.promote(); return {}; }), @@ -4441,7 +3036,7 @@ update(PauseScopedObjectActor.prototype, { * The protocol request object. */ onRelease: PauseScopedActor.withPaused(function (aRequest) { - if (this.registeredPool !== this.threadActor.threadLifetimePool) { + if (this.hooks.isThreadLifetimePool()) { return { error: "notReleasable", message: "Only thread-lifetime actors can be released." }; } @@ -4455,81 +3050,6 @@ update(PauseScopedObjectActor.prototype.requestTypes, { "threadGrip": PauseScopedObjectActor.prototype.onThreadGrip, }); - -/** - * Creates an actor for the specied "very long" string. "Very long" is specified - * at the server's discretion. - * - * @param aString String - * The string. - */ -function LongStringActor(aString) -{ - this.string = aString; - this.stringLength = aString.length; -} - -LongStringActor.prototype = { - - actorPrefix: "longString", - - disconnect: function () { - // Because longStringActors is not a weak map, we won't automatically leave - // it so we need to manually leave on disconnect so that we don't leak - // memory. - if (this.registeredPool && this.registeredPool.longStringActors) { - delete this.registeredPool.longStringActors[this.actorID]; - } - }, - - /** - * Returns a grip for this actor for returning in a protocol message. - */ - grip: function () { - return { - "type": "longString", - "initial": this.string.substring( - 0, DebuggerServer.LONG_STRING_INITIAL_LENGTH), - "length": this.stringLength, - "actor": this.actorID - }; - }, - - /** - * Handle a request to extract part of this actor's string. - * - * @param aRequest object - * The protocol request object. - */ - onSubstring: function (aRequest) { - return { - "from": this.actorID, - "substring": this.string.substring(aRequest.start, aRequest.end) - }; - }, - - /** - * Handle a request to release this LongStringActor instance. - */ - onRelease: function () { - // TODO: also check if registeredPool === threadActor.threadLifetimePool - // when the web console moves aray from manually releasing pause-scoped - // actors. - if (this.registeredPool.longStringActors) { - delete this.registeredPool.longStringActors[this.actorID]; - } - this.registeredPool.removeActor(this); - return {}; - }, -}; - -LongStringActor.prototype.requestTypes = { - "substring": LongStringActor.prototype.onSubstring, - "release": LongStringActor.prototype.onRelease -}; - -exports.LongStringActor = LongStringActor; - /** * Creates an actor for the specified stack frame. * @@ -4576,7 +3096,8 @@ FrameActor.prototype = { let form = { actor: this.actorID, type: this.frame.type }; if (this.frame.type === "call") { - form.callee = threadActor.createValueGrip(this.frame.callee); + form.callee = createValueGrip(this.frame.callee, threadActor._pausePool, + threadActor.objectGrip); } if (this.frame.environment) { @@ -4586,7 +3107,8 @@ FrameActor.prototype = { ); form.environment = envActor.form(); } - form.this = threadActor.createValueGrip(this.frame.this); + form.this = createValueGrip(this.frame.this, threadActor._pausePool, + threadActor.objectGrip); form.arguments = this._args(); if (this.frame.script) { var generatedLocation = this.threadActor.sources.getFrameLocation(this.frame); @@ -4609,7 +3131,8 @@ FrameActor.prototype = { return []; } - return this.frame.arguments.map(arg => this.threadActor.createValueGrip(arg)); + return this.frame.arguments.map(arg => createValueGrip(arg, + this.threadActor._pausePool, this.threadActor.objectGrip)); }, /** @@ -4850,12 +3373,14 @@ EnvironmentActor.prototype = { // Does this environment reflect the properties of an object as variables? if (this.obj.type == "object" || this.obj.type == "with") { - form.object = this.threadActor.createValueGrip(this.obj.object); + form.object = createValueGrip(this.obj.object, + this.registeredPool, this.threadActor.objectGrip); } // Is this the environment created for a function call? if (this.obj.callee) { - form.function = this.threadActor.createValueGrip(this.obj.callee); + form.function = createValueGrip(this.obj.callee, + this.registeredPool, this.threadActor.objectGrip); } // Shall we list this environment's bindings? @@ -4905,11 +3430,14 @@ EnvironmentActor.prototype = { configurable: desc.configurable }; if ("value" in desc) { - descForm.value = this.threadActor.createValueGrip(desc.value); + descForm.value = createValueGrip(desc.value, + this.registeredPool, this.threadActor.objectGrip); descForm.writable = desc.writable; } else { - descForm.get = this.threadActor.createValueGrip(desc.get); - descForm.set = this.threadActor.createValueGrip(desc.set); + descForm.get = createValueGrip(desc.get, this.registeredPool, + this.threadActor.objectGrip); + descForm.set = createValueGrip(desc.set, this.registeredPool, + this.threadActor.objectGrip); } arg[name] = descForm; bindings.arguments.push(arg); @@ -4942,11 +3470,14 @@ EnvironmentActor.prototype = { configurable: desc.configurable }; if ("value" in desc) { - descForm.value = this.threadActor.createValueGrip(desc.value); + descForm.value = createValueGrip(desc.value, + this.registeredPool, this.threadActor.objectGrip); descForm.writable = desc.writable; } else { - descForm.get = this.threadActor.createValueGrip(desc.get || undefined); - descForm.set = this.threadActor.createValueGrip(desc.set || undefined); + descForm.get = createValueGrip(desc.get || undefined, + this.registeredPool, this.threadActor.objectGrip); + descForm.set = createValueGrip(desc.set || undefined, + this.registeredPool, this.threadActor.objectGrip); } bindings.variables[name] = descForm; } @@ -5131,33 +3662,6 @@ reportError = function(aError, aPrefix="") { dumpn(msg); } -/** - * Make a debuggee value for the given object, if needed. Primitive values - * are left the same. - * - * Use case: you have a raw JS object (after unsafe dereference) and you want to - * send it to the client. In that case you need to use an ObjectActor which - * requires a debuggee value. The Debugger.Object.prototype.makeDebuggeeValue() - * method works only for JS objects and functions. - * - * @param Debugger.Object obj - * @param any value - * @return object - */ -function makeDebuggeeValueIfNeeded(obj, value) { - if (value && (typeof value == "object" || typeof value == "function")) { - return obj.makeDebuggeeValue(value); - } - return value; -} - -const symbolProtoToString = Symbol.prototype.toString; - -function getSymbolName(symbol) { - const name = symbolProtoToString.call(symbol).slice("Symbol(".length, -1); - return name || undefined; -} - function isEvalSource(source) { let introType = source.introductionType; // These are all the sources that are essentially eval-ed (either @@ -5244,3 +3748,32 @@ function setBreakpointAtEntryPoints(actor, entryPoints) { } } } + +/** + * Unwrap a global that is wrapped in a |Debugger.Object|, or if the global has + * become a dead object, return |undefined|. + * + * @param Debugger.Object wrappedGlobal + * The |Debugger.Object| which wraps a global. + * + * @returns {Object|undefined} + * Returns the unwrapped global object or |undefined| if unwrapping + * failed. + */ +exports.unwrapDebuggerObjectGlobal = wrappedGlobal => { + try { + // Because of bug 991399 we sometimes get nuked window references here. We + // just bail out in that case. + // + // Note that addon sandboxes have a DOMWindow as their prototype. So make + // sure that we can touch the prototype too (whatever it is), in case _it_ + // is it a nuked window reference. We force stringification to make sure + // that any dead object proxies make themselves known. + let global = wrappedGlobal.unsafeDereference(); + Object.getPrototypeOf(global) + ""; + return global; + } + catch (e) { + return undefined; + } +}; diff --git a/toolkit/devtools/server/actors/webaudio.js b/toolkit/devtools/server/actors/webaudio.js index 88ebe58b5c..29f39287eb 100644 --- a/toolkit/devtools/server/actors/webaudio.js +++ b/toolkit/devtools/server/actors/webaudio.js @@ -12,7 +12,7 @@ const events = require("sdk/event/core"); const { on: systemOn, off: systemOff } = require("sdk/system/events"); const protocol = require("devtools/server/protocol"); const { CallWatcherActor, CallWatcherFront } = require("devtools/server/actors/call-watcher"); -const { ThreadActor } = require("devtools/server/actors/script"); +const { createValueGrip } = require("devtools/server/actors/object"); const AutomationTimeline = require("./utils/automation-timeline"); const { on, once, off, emit } = events; const { types, method, Arg, Option, RetVal } = protocol; @@ -321,13 +321,8 @@ let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({ // AudioBuffer or Float32Array references and the like, // so this just formats the value to be displayed in the VariablesView, // without using real grips and managing via actor pools. - let grip; - try { - grip = ThreadActor.prototype.createValueGrip(value); - } - catch (e) { - grip = createObjectGrip(value); - } + let grip = createValueGrip(value, null, createObjectGrip); + return grip; }, { request: { diff --git a/toolkit/devtools/server/actors/webbrowser.js b/toolkit/devtools/server/actors/webbrowser.js index 49aa3e49be..f7aa26bdf8 100644 --- a/toolkit/devtools/server/actors/webbrowser.js +++ b/toolkit/devtools/server/actors/webbrowser.js @@ -13,15 +13,15 @@ let { ActorPool, createExtraActors, appendExtraActors } = require("devtools/serv let { DebuggerServer } = require("devtools/server/main"); let DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); let { dbg_assert } = DevToolsUtils; -let { TabSources, isHiddenSource } = require("./utils/TabSources"); +let { TabSources } = require("./utils/TabSources"); let makeDebugger = require("./utils/make-debugger"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); loader.lazyRequireGetter(this, "RootActor", "devtools/server/actors/root", true); -loader.lazyRequireGetter(this, "AddonThreadActor", "devtools/server/actors/script", true); loader.lazyRequireGetter(this, "ThreadActor", "devtools/server/actors/script", true); -loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id"); +loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true); +loader.lazyRequireGetter(this, "BrowserAddonActor", "devtools/server/actors/addon", true); loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); // Assumptions on events module: @@ -111,35 +111,6 @@ function sendShutdownEvent() { exports.sendShutdownEvent = sendShutdownEvent; -/** - * Unwrap a global that is wrapped in a |Debugger.Object|, or if the global has - * become a dead object, return |undefined|. - * - * @param Debugger.Object wrappedGlobal - * The |Debugger.Object| which wraps a global. - * - * @returns {Object|undefined} - * Returns the unwrapped global object or |undefined| if unwrapping - * failed. - */ -const unwrapDebuggerObjectGlobal = wrappedGlobal => { - try { - // Because of bug 991399 we sometimes get nuked window references here. We - // just bail out in that case. - // - // Note that addon sandboxes have a DOMWindow as their prototype. So make - // sure that we can touch the prototype too (whatever it is), in case _it_ - // is it a nuked window reference. We force stringification to make sure - // that any dead object proxies make themselves known. - let global = wrappedGlobal.unsafeDereference(); - Object.getPrototypeOf(global) + ""; - return global; - } - catch (e) { - return undefined; - } -}; - /** * Construct a root actor appropriate for use in a server running in a * browser. The returned root actor: @@ -1848,238 +1819,6 @@ BrowserAddonList.prototype.onUninstalled = function (aAddon) { exports.BrowserAddonList = BrowserAddonList; -function BrowserAddonActor(aConnection, aAddon) { - this.conn = aConnection; - this._addon = aAddon; - this._contextPool = new ActorPool(this.conn); - this.conn.addActorPool(this._contextPool); - this._threadActor = null; - this._global = null; - - this._shouldAddNewGlobalAsDebuggee = this._shouldAddNewGlobalAsDebuggee.bind(this); - - this.makeDebugger = makeDebugger.bind(null, { - findDebuggees: this._findDebuggees.bind(this), - shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee - }); - - AddonManager.addAddonListener(this); -} - -BrowserAddonActor.prototype = { - actorPrefix: "addon", - - get exited() { - return !this._addon; - }, - - get id() { - return this._addon.id; - }, - - get url() { - return this._addon.sourceURI ? this._addon.sourceURI.spec : undefined; - }, - - get attached() { - return this._threadActor; - }, - - get global() { - return this._global; - }, - - get sources() { - if (!this._sources) { - dbg_assert(this.threadActor, "threadActor should exist when creating sources."); - this._sources = new TabSources(this._threadActor, this._allowSource); - } - return this._sources; - }, - - - form: function BAA_form() { - dbg_assert(this.actorID, "addon should have an actorID."); - if (!this._consoleActor) { - let {AddonConsoleActor} = require("devtools/server/actors/webconsole"); - this._consoleActor = new AddonConsoleActor(this._addon, this.conn, this); - this._contextPool.addActor(this._consoleActor); - } - - return { - actor: this.actorID, - id: this.id, - name: this._addon.name, - url: this.url, - debuggable: this._addon.isDebuggable, - consoleActor: this._consoleActor.actorID, - - traits: { - highlightable: false, - networkMonitor: false, - }, - }; - }, - - disconnect: function BAA_disconnect() { - this.conn.removeActorPool(this._contextPool); - this._contextPool = null; - this._consoleActor = null; - this._addon = null; - this._global = null; - AddonManager.removeAddonListener(this); - }, - - setOptions: function BAA_setOptions(aOptions) { - if ("global" in aOptions) { - this._global = aOptions.global; - } - }, - - onDisabled: function BAA_onDisabled(aAddon) { - if (aAddon != this._addon) { - return; - } - - this._global = null; - }, - - onUninstalled: function BAA_onUninstalled(aAddon) { - if (aAddon != this._addon) { - return; - } - - if (this.attached) { - this.onDetach(); - this.conn.send({ from: this.actorID, type: "tabDetached" }); - } - - this.disconnect(); - }, - - onAttach: function BAA_onAttach() { - if (this.exited) { - return { type: "exited" }; - } - - if (!this.attached) { - this._threadActor = new AddonThreadActor(this.conn, this); - this._contextPool.addActor(this._threadActor); - } - - return { type: "tabAttached", threadActor: this._threadActor.actorID }; - }, - - onDetach: function BAA_onDetach() { - if (!this.attached) { - return { error: "wrongState" }; - } - - this._contextPool.removeActor(this._threadActor); - - this._threadActor = null; - this._sources = null; - - return { type: "detached" }; - }, - - preNest: function() { - let e = Services.wm.getEnumerator(null); - while (e.hasMoreElements()) { - let win = e.getNext(); - let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - windowUtils.suppressEventHandling(true); - windowUtils.suspendTimeouts(); - } - }, - - postNest: function() { - let e = Services.wm.getEnumerator(null); - while (e.hasMoreElements()) { - let win = e.getNext(); - let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - windowUtils.resumeTimeouts(); - windowUtils.suppressEventHandling(false); - } - }, - - /** - * Return true if the given global is associated with this addon and should be - * added as a debuggee, false otherwise. - */ - _shouldAddNewGlobalAsDebuggee: function (aGlobal) { - const global = unwrapDebuggerObjectGlobal(aGlobal); - try { - // This will fail for non-Sandbox objects, hence the try-catch block. - let metadata = Cu.getSandboxMetadata(global); - if (metadata) { - return metadata.addonID === this.id; - } - } catch (e) {} - - if (global instanceof Ci.nsIDOMWindow) { - let id = {}; - if (mapURIToAddonID(global.document.documentURIObject, id)) { - return id.value === this.id; - } - return false; - } - - // Check the global for a __URI__ property and then try to map that to an - // add-on - let uridescriptor = aGlobal.getOwnPropertyDescriptor("__URI__"); - if (uridescriptor && "value" in uridescriptor && uridescriptor.value) { - let uri; - try { - uri = Services.io.newURI(uridescriptor.value, null, null); - } - catch (e) { - DevToolsUtils.reportException( - "BrowserAddonActor.prototype._shouldAddNewGlobalAsDebuggee", - new Error("Invalid URI: " + uridescriptor.value) - ); - return false; - } - - let id = {}; - if (mapURIToAddonID(uri, id)) { - return id.value === this.id; - } - } - - return false; - }, - - /** - * Override the eligibility check for scripts and sources to make - * sure every script and source with a URL is stored when debugging - * add-ons. - */ - _allowSource: function(aSource) { - // XPIProvider.jsm evals some code in every add-on's bootstrap.js. Hide it. - if (aSource.url === "resource://gre/modules/addons/XPIProvider.jsm") { - return false; - } - - return true; - }, - - /** - * Yield the current set of globals associated with this addon that should be - * added as debuggees. - */ - _findDebuggees: function (dbg) { - return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee); - } -}; - -BrowserAddonActor.prototype.requestTypes = { - "attach": BrowserAddonActor.prototype.onAttach, - "detach": BrowserAddonActor.prototype.onDetach -}; - /** * The DebuggerProgressListener object is an nsIWebProgressListener which * handles onStateChange events for the inspected browser. If the user tries to diff --git a/toolkit/devtools/server/actors/webconsole.js b/toolkit/devtools/server/actors/webconsole.js index 3c9303e938..3ef22bdac9 100644 --- a/toolkit/devtools/server/actors/webconsole.js +++ b/toolkit/devtools/server/actors/webconsole.js @@ -8,8 +8,9 @@ const { Cc, Ci, Cu } = require("chrome"); const { DebuggerServer, ActorPool } = require("devtools/server/main"); -const { EnvironmentActor, LongStringActor, ObjectActor, ThreadActor } = require("devtools/server/actors/script"); -const { update } = require("devtools/toolkit/DevToolsUtils"); +const { EnvironmentActor, ThreadActor } = require("devtools/server/actors/script"); +const { ObjectActor, LongStringActor, createValueGrip, stringIsLong } = require("devtools/server/actors/object"); +const DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -71,7 +72,9 @@ function WebConsoleActor(aConnection, aParentActor) this._netEvents = new Map(); this._gripDepth = 0; this._listeners = new Set(); + this._lastConsoleInputEvaluation = undefined; + this.objectGrip = this.objectGrip.bind(this); this._onWillNavigate = this._onWillNavigate.bind(this); this._onChangedToplevelDocument = this._onChangedToplevelDocument.bind(this); events.on(this.parentActor, "changed-toplevel-document", this._onChangedToplevelDocument); @@ -302,6 +305,10 @@ WebConsoleActor.prototype = actorPrefix: "console", + get globalDebugObject() { + return this.parentActor.threadActor.globalDebugObject; + }, + grip: function WCA_grip() { return { actor: this.actorID }; @@ -319,8 +326,6 @@ WebConsoleActor.prototype = return isNative; }, - _createValueGrip: ThreadActor.prototype.createValueGrip, - _stringIsLong: ThreadActor.prototype._stringIsLong, _findProtoChain: ThreadActor.prototype._findProtoChain, _removeFromProtoChain: ThreadActor.prototype._removeFromProtoChain, @@ -358,6 +363,7 @@ WebConsoleActor.prototype = this._actorPool = null; this._jstermHelpersCache = null; + this._lastConsoleInputEvaluation = null; this._evalWindow = null; this._netEvents.clear(); this.dbg.enabled = false; @@ -400,7 +406,7 @@ WebConsoleActor.prototype = */ createValueGrip: function WCA_createValueGrip(aValue) { - return this._createValueGrip(aValue, this._actorPool); + return createValueGrip(aValue, this._actorPool, this.objectGrip); }, /** @@ -443,7 +449,16 @@ WebConsoleActor.prototype = */ objectGrip: function WCA_objectGrip(aObject, aPool) { - let actor = new ObjectActor(aObject, this); + let actor = new ObjectActor(aObject, { + getGripDepth: () => this._gripDepth, + incrementGripDepth: () => this._gripDepth++, + decrementGripDepth: () => this._gripDepth--, + createValueGrip: v => this.createValueGrip(v), + sources: () => DevToolsUtils.reportException("WebConsoleActor", + Error("sources not yet implemented")), + createEnvironmentActor: (env) => this.createEnvironmentActor(env), + getGlobalDebugObject: () => this.globalDebugObject + }); aPool.addActor(actor); return actor.grip(); }, @@ -460,7 +475,7 @@ WebConsoleActor.prototype = */ longStringGrip: function WCA_longStringGrip(aString, aPool) { - let actor = new LongStringActor(aString, this); + let actor = new LongStringActor(aString); aPool.addActor(actor); return actor.grip(); }, @@ -477,7 +492,7 @@ WebConsoleActor.prototype = */ _createStringGrip: function NEA__createStringGrip(aString) { - if (aString && this._stringIsLong(aString)) { + if (aString && stringIsLong(aString)) { return this.longStringGrip(aString, this._actorPool); } return aString; @@ -505,6 +520,17 @@ WebConsoleActor.prototype = this._actorPool.removeActor(aActor.actorID); }, + /** + * Returns the latest web console input evaluation. + * This is undefined if no evaluations have been completed. + * + * @return object + */ + getLastConsoleInputEvaluation: function WCU_getLastConsoleInputEvaluation() + { + return this._lastConsoleInputEvaluation; + }, + ////////////////// // Request handlers for known packet types. ////////////////// @@ -818,6 +844,8 @@ WebConsoleActor.prototype = errorMessage = e; } + this._lastConsoleInputEvaluation = result; + return { from: this.actorID, input: input, @@ -1527,91 +1555,6 @@ WebConsoleActor.prototype.requestTypes = exports.WebConsoleActor = WebConsoleActor; - -/** - * The AddonConsoleActor implements capabilities needed for the add-on web - * console feature. - * - * @constructor - * @param object aAddon - * The add-on that this console watches. - * @param object aConnection - * The connection to the client, DebuggerServerConnection. - * @param object aParentActor - * The parent BrowserAddonActor actor. - */ -function AddonConsoleActor(aAddon, aConnection, aParentActor) -{ - this.addon = aAddon; - WebConsoleActor.call(this, aConnection, aParentActor); -} - -AddonConsoleActor.prototype = Object.create(WebConsoleActor.prototype); - -update(AddonConsoleActor.prototype, { - constructor: AddonConsoleActor, - - actorPrefix: "addonConsole", - - /** - * The add-on that this console watches. - */ - addon: null, - - /** - * The main add-on JS global - */ - get window() { - return this.parentActor.global; - }, - - /** - * Destroy the current AddonConsoleActor instance. - */ - disconnect: function ACA_disconnect() - { - WebConsoleActor.prototype.disconnect.call(this); - this.addon = null; - }, - - /** - * Handler for the "startListeners" request. - * - * @param object aRequest - * The JSON request object received from the Web Console client. - * @return object - * The response object which holds the startedListeners array. - */ - onStartListeners: function ACA_onStartListeners(aRequest) - { - let startedListeners = []; - - while (aRequest.listeners.length > 0) { - let listener = aRequest.listeners.shift(); - switch (listener) { - case "ConsoleAPI": - if (!this.consoleAPIListener) { - this.consoleAPIListener = - new ConsoleAPIListener(null, this, "addon/" + this.addon.id); - this.consoleAPIListener.init(); - } - startedListeners.push(listener); - break; - } - } - return { - startedListeners: startedListeners, - nativeConsoleAPI: true, - traits: this.traits, - }; - }, -}); - -AddonConsoleActor.prototype.requestTypes = Object.create(WebConsoleActor.prototype.requestTypes); -AddonConsoleActor.prototype.requestTypes.startListeners = AddonConsoleActor.prototype.onStartListeners; - -exports.AddonConsoleActor = AddonConsoleActor; - /** * Creates an actor for a network event. * diff --git a/toolkit/devtools/server/moz.build b/toolkit/devtools/server/moz.build index 1e154316fb..f3636a5a65 100644 --- a/toolkit/devtools/server/moz.build +++ b/toolkit/devtools/server/moz.build @@ -53,6 +53,7 @@ EXTRA_JS_MODULES.devtools.server += [ EXTRA_JS_MODULES.devtools.server.actors += [ 'actors/actor-registry.js', + 'actors/addon.js', 'actors/animation.js', 'actors/call-watcher.js', 'actors/canvas.js', @@ -72,6 +73,7 @@ EXTRA_JS_MODULES.devtools.server.actors += [ 'actors/layout.js', 'actors/memory.js', 'actors/monitor.js', + 'actors/object.js', 'actors/preference.js', 'actors/pretty-print-worker.js', 'actors/profiler.js', diff --git a/toolkit/devtools/server/tests/unit/test_longstringactor.js b/toolkit/devtools/server/tests/unit/test_longstringactor.js index 4a0b80c419..bf017ee88d 100644 --- a/toolkit/devtools/server/tests/unit/test_longstringactor.js +++ b/toolkit/devtools/server/tests/unit/test_longstringactor.js @@ -2,7 +2,7 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ -const { LongStringActor } = devtools.require("devtools/server/actors/script"); +const { LongStringActor } = devtools.require("devtools/server/actors/object"); function run_test() { @@ -23,7 +23,7 @@ function makeMockLongStringActor() actor.actorID = "longString1"; actor.registeredPool = { longStringActors: { - longString1: actor + [string]: actor } }; return actor; @@ -32,10 +32,10 @@ function makeMockLongStringActor() function test_LSA_disconnect() { let actor = makeMockLongStringActor(); - do_check_eq(actor.registeredPool.longStringActors[actor.actorID], actor); + do_check_eq(actor.registeredPool.longStringActors[TEST_STRING], actor); actor.disconnect(); - do_check_eq(actor.registeredPool.longStringActors[actor.actorID], void 0); + do_check_eq(actor.registeredPool.longStringActors[TEST_STRING], void 0); } function test_LSA_substring() diff --git a/toolkit/devtools/server/tests/unit/test_promise_actor_list_promises.js b/toolkit/devtools/server/tests/unit/test_promise_actor_list_promises.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/toolkit/devtools/server/tests/unit/test_promises_actor_list_promises.js b/toolkit/devtools/server/tests/unit/test_promises_actor_list_promises.js index a2b115e17c..ed97170660 100644 --- a/toolkit/devtools/server/tests/unit/test_promises_actor_list_promises.js +++ b/toolkit/devtools/server/tests/unit/test_promises_actor_list_promises.js @@ -43,6 +43,10 @@ function* testListPromises(client, form, makePromise) { for (let p of promises) { equal(p.type, "object", "Expect type to be Object"); equal(p.class, "Promise", "Expect class to be Promise"); + equal(typeof p.promiseState.creationTimestamp, "number", + "Expect creation timestamp to be a number"); + equal(typeof p.promiseState.timeToSettle, "number", + "Expect time to settle to be a number"); if (p.promiseState.state === "fulfilled" && p.promiseState.value === resolution) { diff --git a/toolkit/devtools/server/tests/unit/test_promises_actor_onnewpromise.js b/toolkit/devtools/server/tests/unit/test_promises_actor_onnewpromise.js index 8b2401a481..d53792aff9 100644 --- a/toolkit/devtools/server/tests/unit/test_promises_actor_onnewpromise.js +++ b/toolkit/devtools/server/tests/unit/test_promises_actor_onnewpromise.js @@ -46,6 +46,8 @@ function* testNewPromisesEvent(client, form, makePromise) { for (let p of promises) { equal(p.type, "object", "Expect type to be Object"); equal(p.class, "Promise", "Expect class to be Promise"); + equal(typeof p.promiseState.creationTimestamp, "number", + "Expect creation timestamp to be a number"); if (p.promiseState.state === "fulfilled" && p.promiseState.value === resolution) { diff --git a/toolkit/devtools/server/tests/unit/test_promises_actor_onpromisesettled.js b/toolkit/devtools/server/tests/unit/test_promises_actor_onpromisesettled.js deleted file mode 100644 index 75ad134b66..0000000000 --- a/toolkit/devtools/server/tests/unit/test_promises_actor_onpromisesettled.js +++ /dev/null @@ -1,83 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -/** - * Test that we can get the list of Promise objects that have settled from the - * PromisesActor onPromiseSettled event handler. - */ - -"use strict"; - -const { PromisesFront } = devtools.require("devtools/server/actors/promises"); - -let events = devtools.require("sdk/event/core"); - -add_task(function*() { - let client = yield startTestDebuggerServer("promises-actor-test"); - let chromeActors = yield getChromeActors(client); - - ok(Promise.toString().contains("native code"), "Expect native DOM Promise"); - - yield testPromisesSettled(client, chromeActors, - v => new Promise(resolve => resolve(v)), - v => new Promise((resolve, reject) => reject(v))); - - let response = yield listTabs(client); - let targetTab = findTab(response.tabs, "promises-actor-test"); - ok(targetTab, "Found our target tab."); - - yield testPromisesSettled(client, targetTab, v => { - const debuggee = DebuggerServer.getTestGlobal("promises-actor-test"); - return debuggee.Promise.resolve(v); - }, v => { - const debuggee = DebuggerServer.getTestGlobal("promises-actor-test"); - return debuggee.Promise.reject(v); - }); - - yield close(client); -}); - -function* testPromisesSettled(client, form, makeResolvePromise, - makeRejectPromise) { - let front = PromisesFront(client, form); - let resolution = "MyLittleSecret" + Math.random(); - - yield front.attach(); - yield front.listPromises(); - - let onPromiseSettled = oncePromiseSettled(front, resolution, true, false); - let resolvedPromise = makeResolvePromise(resolution); - let foundResolvedPromise = yield onPromiseSettled; - ok(foundResolvedPromise, "Found our resolved promise"); - - onPromiseSettled = oncePromiseSettled(front, resolution, false, true); - let rejectedPromise = makeRejectPromise(resolution); - let foundRejectedPromise = yield onPromiseSettled; - ok(foundRejectedPromise, "Found our rejected promise"); - - yield front.detach(); - // Appease eslint - void resolvedPromise; - void rejectedPromise; -} - -function oncePromiseSettled(front, resolution, resolveValue, rejectValue) { - return new Promise(resolve => { - events.on(front, "promises-settled", promises => { - for (let p of promises) { - equal(p.type, "object", "Expect type to be Object"); - equal(p.class, "Promise", "Expect class to be Promise"); - - if (p.promiseState.state === "fulfilled" && - p.promiseState.value === resolution) { - resolve(resolveValue); - } else if (p.promiseState.state === "rejected" && - p.promiseState.reason === resolution) { - resolve(rejectValue); - } else { - dump("Found non-target promise\n"); - } - } - }); - }); -} diff --git a/toolkit/devtools/server/tests/unit/test_promises_client_getdependentpromises.js b/toolkit/devtools/server/tests/unit/test_promises_client_getdependentpromises.js new file mode 100644 index 0000000000..3f4ebcfe81 --- /dev/null +++ b/toolkit/devtools/server/tests/unit/test_promises_client_getdependentpromises.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we can get the list of dependent promises from the ObjectClient. + */ + +"use strict"; + +const { PromisesFront } = devtools.require("devtools/server/actors/promises"); + +let events = devtools.require("sdk/event/core"); + +add_task(function*() { + let client = yield startTestDebuggerServer("test-promises-dependentpromises"); + let chromeActors = yield getChromeActors(client); + + ok(Promise.toString().contains("native code"), "Expect native DOM Promise."); + + yield testGetDependentPromises(client, chromeActors, () => { + let p = new Promise(() => {}); + p.name = "p"; + let q = p.then(); + q.name = "q"; + let r = p.then(null, () => {}); + r.name = "r"; + + return p; + }); + + let response = yield listTabs(client); + let targetTab = findTab(response.tabs, "test-promises-dependentpromises"); + ok(targetTab, "Found our target tab."); + + yield testGetDependentPromises(client, targetTab, () => { + const debuggee = + DebuggerServer.getTestGlobal("test-promises-dependentpromises"); + + let p = new debuggee.Promise(() => {}); + p.name = "p"; + let q = p.then(); + q.name = "q"; + let r = p.then(null, () => {}); + r.name = "r"; + + return p; + }); + + yield close(client); +}); + +function* testGetDependentPromises(client, form, makePromises) { + let front = PromisesFront(client, form); + + yield front.attach(); + yield front.listPromises(); + + // Get the grip for promise p + let onNewPromise = new Promise(resolve => { + events.on(front, "new-promises", promises => { + for (let p of promises) { + if (p.preview.ownProperties.name && + p.preview.ownProperties.name.value === "p") { + resolve(p); + } + } + }); + }); + + let promise = makePromises(); + + let grip = yield onNewPromise; + ok(grip, "Found our promise p."); + + let objectClient = new ObjectClient(client, grip); + ok(objectClient, "Got Object Client."); + + // Get the dependent promises for promise p and assert that the list of + // dependent promises is correct + yield new Promise(resolve => { + objectClient.getDependentPromises(response => { + let dependentNames = response.promises.map(p => + p.preview.ownProperties.name.value); + let expectedDependentNames = ["q", "r"]; + + equal(dependentNames.length, expectedDependentNames.length, + "Got expected number of dependent promises."); + + for (let i = 0; i < dependentNames.length; i++) { + equal(dependentNames[i], expectedDependentNames[i], + "Got expected dependent name."); + } + + for (let p of response.promises) { + equal(p.type, "object", "Expect type to be Object."); + equal(p.class, "Promise", "Expect class to be Promise."); + equal(typeof p.promiseState.creationTimestamp, "number", + "Expect creation timestamp to be a number."); + ok(!p.promiseState.timeToSettle, + "Expect time to settle to be undefined."); + } + + resolve(); + }); + }); + + yield front.detach(); + // Appease eslint + void promise; +} diff --git a/toolkit/devtools/server/tests/unit/test_promises_object_creationtimestamp.js b/toolkit/devtools/server/tests/unit/test_promises_object_creationtimestamp.js new file mode 100644 index 0000000000..f87e48ad6f --- /dev/null +++ b/toolkit/devtools/server/tests/unit/test_promises_object_creationtimestamp.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get the approximate time range for promise creation timestamp. + */ + +"use strict"; + +const { PromisesFront } = devtools.require("devtools/server/actors/promises"); + +let events = devtools.require("sdk/event/core"); + +add_task(function*() { + let client = yield startTestDebuggerServer("promises-object-test"); + let chromeActors = yield getChromeActors(client); + + ok(Promise.toString().contains("native code"), "Expect native DOM Promise."); + + yield testPromiseCreationTimestamp(client, chromeActors, v => { + return new Promise(resolve => resolve(v)); + }); + + let response = yield listTabs(client); + let targetTab = findTab(response.tabs, "promises-object-test"); + ok(targetTab, "Found our target tab."); + + yield testPromiseCreationTimestamp(client, targetTab, v => { + const debuggee = DebuggerServer.getTestGlobal("promises-object-test"); + return debuggee.Promise.resolve(v); + }); + + yield close(client); +}); + +function* testPromiseCreationTimestamp(client, form, makePromise) { + let front = PromisesFront(client, form); + let resolution = "MyLittleSecret" + Math.random(); + + yield front.attach(); + yield front.listPromises(); + + let onNewPromise = new Promise(resolve => { + events.on(front, "new-promises", promises => { + for (let p of promises) { + if (p.promiseState.state === "fulfilled" && + p.promiseState.value === resolution) { + resolve(p); + } + } + }); + }); + + let start = Date.now(); + let promise = makePromise(resolution); + let end = Date.now(); + + let grip = yield onNewPromise; + ok(grip, "Found our new promise."); + + let creationTimestamp = grip.promiseState.creationTimestamp; + + ok(start - 1 <= creationTimestamp && creationTimestamp <= end + 1, + "Expect promise creation timestamp to be within elapsed time range."); + + yield front.detach(); + // Appease eslint + void promise; +} diff --git a/toolkit/devtools/server/tests/unit/test_promises_object_timetosettle-01.js b/toolkit/devtools/server/tests/unit/test_promises_object_timetosettle-01.js new file mode 100644 index 0000000000..d317783db4 --- /dev/null +++ b/toolkit/devtools/server/tests/unit/test_promises_object_timetosettle-01.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test whether or not we get the time to settle depending on the state of the + * promise. + */ + +"use strict"; + +const { PromisesFront } = devtools.require("devtools/server/actors/promises"); + +let events = devtools.require("sdk/event/core"); + +add_task(function*() { + let client = yield startTestDebuggerServer("test-promises-timetosettle"); + let chromeActors = yield getChromeActors(client); + + ok(Promise.toString().contains("native code"), "Expect native DOM Promise."); + + yield testGetTimeToSettle(client, chromeActors, () => { + let p = new Promise(() => {}); + p.name = "p"; + let q = p.then(); + q.name = "q"; + + return p; + }); + + let response = yield listTabs(client); + let targetTab = findTab(response.tabs, "test-promises-timetosettle"); + ok(targetTab, "Found our target tab."); + + yield testGetTimeToSettle(client, targetTab, () => { + const debuggee = + DebuggerServer.getTestGlobal("test-promises-timetosettle"); + + let p = new debuggee.Promise(() => {}); + p.name = "p"; + let q = p.then(); + q.name = "q"; + + return p; + }); + + yield close(client); +}); + +function* testGetTimeToSettle(client, form, makePromises) { + let front = PromisesFront(client, form); + + yield front.attach(); + yield front.listPromises(); + + let onNewPromise = new Promise(resolve => { + events.on(front, "new-promises", promises => { + for (let p of promises) { + if (p.promiseState.state === "pending") { + ok(!p.promiseState.timeToSettle, + "Expect no time to settle for unsettled promise."); + } else { + ok(p.promiseState.timeToSettle, + "Expect time to settle for settled promise."); + equal(typeof p.promiseState.timeToSettle, "number", + "Expect time to settle to be a number."); + } + } + resolve(); + }); + }); + + let promise = makePromises(); + + yield onNewPromise; + yield front.detach(); + // Appease eslint + void promise; +} diff --git a/toolkit/devtools/server/tests/unit/test_promises_object_timetosettle-02.js b/toolkit/devtools/server/tests/unit/test_promises_object_timetosettle-02.js new file mode 100644 index 0000000000..4d59ebd7da --- /dev/null +++ b/toolkit/devtools/server/tests/unit/test_promises_object_timetosettle-02.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that we get the expected settlement time for promise time to settle. + */ + +"use strict"; + +const { PromisesFront } = devtools.require("devtools/server/actors/promises"); +const { setTimeout } = devtools.require("sdk/timers"); + +let events = devtools.require("sdk/event/core"); + +add_task(function*() { + let client = yield startTestDebuggerServer("test-promises-timetosettle"); + let chromeActors = yield getChromeActors(client); + + ok(Promise.toString().contains("native code"), "Expect native DOM Promise."); + + yield testGetTimeToSettle(client, chromeActors, + v => new Promise(resolve => setTimeout(() => resolve(v), 100))); + + let response = yield listTabs(client); + let targetTab = findTab(response.tabs, "test-promises-timetosettle"); + ok(targetTab, "Found our target tab."); + + yield testGetTimeToSettle(client, targetTab, v => { + const debuggee = + DebuggerServer.getTestGlobal("test-promises-timetosettle"); + return new debuggee.Promise(resolve => setTimeout(() => resolve(v), 100)); + }); + + yield close(client); +}); + +function* testGetTimeToSettle(client, form, makePromise) { + let front = PromisesFront(client, form); + let resolution = "MyLittleSecret" + Math.random(); + let found = false; + + yield front.attach(); + yield front.listPromises(); + + let onNewPromise = new Promise(resolve => { + events.on(front, "promises-settled", promises => { + for (let p of promises) { + if (p.promiseState.state === "fulfilled" && + p.promiseState.value === resolution) { + equal(Math.floor(p.promiseState.timeToSettle / 100) * 100, 100, + "Expect time to settle for resolved promise to be " + + "approximately 100ms."); + found = true; + resolve(); + } else { + dump("Found non-target promise.\n"); + } + } + }); + }); + + let promise = makePromise(resolution); + + yield onNewPromise; + ok(found, "Found our new promise."); + yield front.detach(); + // Appease eslint + void promise; +} diff --git a/toolkit/devtools/server/tests/unit/testactors.js b/toolkit/devtools/server/tests/unit/testactors.js index 7e09268881..695cc4f997 100644 --- a/toolkit/devtools/server/tests/unit/testactors.js +++ b/toolkit/devtools/server/tests/unit/testactors.js @@ -95,7 +95,7 @@ TestTabActor.prototype = { actorPrefix: "TestTabActor", get window() { - return { wrappedJSObject: this._global }; + return this._global; }, get url() { diff --git a/toolkit/devtools/server/tests/unit/xpcshell.ini b/toolkit/devtools/server/tests/unit/xpcshell.ini index 659136e2a6..f278896307 100644 --- a/toolkit/devtools/server/tests/unit/xpcshell.ini +++ b/toolkit/devtools/server/tests/unit/xpcshell.ini @@ -83,7 +83,10 @@ support-files = [test_promises_actor_exist.js] [test_promises_actor_list_promises.js] [test_promises_actor_onnewpromise.js] -[test_promises_actor_onpromisesettled.js] +[test_promises_client_getdependentpromises.js] +[test_promises_object_creationtimestamp.js] +[test_promises_object_timetosettle-01.js] +[test_promises_object_timetosettle-02.js] [test_protocol_abort.js] [test_protocol_async.js] [test_protocol_children.js] diff --git a/toolkit/devtools/shared/widgets/VariablesView.jsm b/toolkit/devtools/shared/widgets/VariablesView.jsm index 5f8f244f57..0dd106cc36 100644 --- a/toolkit/devtools/shared/widgets/VariablesView.jsm +++ b/toolkit/devtools/shared/widgets/VariablesView.jsm @@ -12,7 +12,6 @@ const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties"; const LAZY_EMPTY_DELAY = 150; // ms const LAZY_EXPAND_DELAY = 50; // ms const SCROLL_PAGE_SIZE_DEFAULT = 0; -const APPEND_PAGE_SIZE_DEFAULT = 500; const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100; const PAGE_SIZE_MAX_JUMPS = 30; const SEARCH_ACTION_MAX_DELAY = 300; // ms @@ -246,12 +245,6 @@ VariablesView.prototype = { */ scrollPageSize: SCROLL_PAGE_SIZE_DEFAULT, - /** - * The maximum number of elements allowed in a scope, variable or property - * that allows pagination when appending children. - */ - appendPageSize: APPEND_PAGE_SIZE_DEFAULT, - /** * Function called each time a variable or property's value is changed via * user interaction. If null, then value changes are disabled. @@ -556,6 +549,14 @@ VariablesView.prototype = { * The variable or property to search for. */ _doSearch: function(aToken) { + if (this.controller.supportsSearch()) { + this.empty(); + let scope = this.addScope(aToken); + scope.expanded = true; // Expand the scope by default. + scope.locked = true; // Prevent collapsing the scope. + this.controller.performSearch(scope, aToken); + return; + } for (let scope of this._store) { switch (aToken) { case "": @@ -1214,7 +1215,6 @@ function Scope(aView, aName, aFlags = {}) { // Inherit properties and flags from the parent view. You can override // each of these directly onto any scope, variable or property instance. this.scrollPageSize = aView.scrollPageSize; - this.appendPageSize = aView.appendPageSize; this.eval = aView.eval; this.switch = aView.switch; this.delete = aView.delete; @@ -1320,82 +1320,13 @@ Scope.prototype = { * Additional options for adding the properties. Supported options: * - sorted: true to sort all the properties before adding them * - callback: function invoked after each item is added - * @param string aKeysType [optional] - * Helper argument in the case of paginated items. Can be either - * "just-strings" or "just-numbers". Humans shouldn't use this argument. */ - addItems: function(aItems, aOptions = {}, aKeysType = "") { + addItems: function(aItems, aOptions = {}) { let names = Object.keys(aItems); - // Building the view when inspecting an object with a very large number of - // properties may take a long time. To avoid blocking the UI, group - // the items into several lazily populated pseudo-items. - let exceedsThreshold = names.length >= this.appendPageSize; - let shouldPaginate = exceedsThreshold && aKeysType != "just-strings"; - if (shouldPaginate && this.allowPaginate) { - // Group the items to append into two separate arrays, one containing - // number-like keys, the other one containing string keys. - if (aKeysType == "just-numbers") { - var numberKeys = names; - var stringKeys = []; - } else { - var numberKeys = []; - var stringKeys = []; - for (let name of names) { - // Be very careful. Avoid Infinity, NaN and non Natural number keys. - let coerced = +name; - if (Number.isInteger(coerced) && coerced > -1) { - numberKeys.push(name); - } else { - stringKeys.push(name); - } - } - } - - // This object contains a very large number of properties, but they're - // almost all strings that can't be coerced to numbers. Don't paginate. - if (numberKeys.length < this.appendPageSize) { - this.addItems(aItems, aOptions, "just-strings"); - return; - } - - // Slices a section of the { name: descriptor } data properties. - let paginate = (aArray, aBegin = 0, aEnd = aArray.length) => { - let store = {} - for (let i = aBegin; i < aEnd; i++) { - let name = aArray[i]; - store[name] = aItems[name]; - } - return store; - }; - - // Creates a pseudo-item that populates itself with the data properties - // from the corresponding page range. - let createRangeExpander = (aArray, aBegin, aEnd, aOptions, aKeyTypes) => { - let rangeVar = this.addItem(aArray[aBegin] + Scope.ellipsis + aArray[aEnd - 1]); - rangeVar.onexpand = () => { - let pageItems = paginate(aArray, aBegin, aEnd); - rangeVar.addItems(pageItems, aOptions, aKeyTypes); - } - rangeVar.showArrow(); - rangeVar.target.setAttribute("pseudo-item", ""); - }; - - // Divide the number keys into quarters. - let page = +Math.round(numberKeys.length / 4).toPrecision(1); - createRangeExpander(numberKeys, 0, page, aOptions, "just-numbers"); - createRangeExpander(numberKeys, page, page * 2, aOptions, "just-numbers"); - createRangeExpander(numberKeys, page * 2, page * 3, aOptions, "just-numbers"); - createRangeExpander(numberKeys, page * 3, numberKeys.length, aOptions, "just-numbers"); - - // Append all the string keys together. - this.addItems(paginate(stringKeys), aOptions, "just-strings"); - return; - } - // Sort all of the properties before adding them, if preferred. - if (aOptions.sorted && aKeysType != "just-numbers") { - names.sort(); + if (aOptions.sorted) { + names.sort(this._naturalSort); } // Add the properties to the current scope. @@ -1536,7 +1467,11 @@ Scope.prototype = { this._isExpanded = true; if (this.onexpand) { - this.onexpand(this); + // We return onexpand as it sometimes returns a promise + // (up to the user of VariableView to do it) + // that can indicate when the view is done expanding + // and attributes are available. (Mostly used for tests) + return this.onexpand(this); } }, @@ -1601,6 +1536,22 @@ Scope.prototype = { this._isHeaderVisible = false; }, + /** + * Sort in ascending order + * This only needs to compare non-numbers since it is dealing with an array + * which numeric-based indices are placed in order. + * + * @param string a + * @param string b + * @return number + * -1 if a is less than b, 0 if no change in order, +1 if a is greater than 0 + */ + _naturalSort: function(a,b) { + if (isNaN(parseFloat(a)) && isNaN(parseFloat(b))) { + return a < b ? -1 : 1; + } + }, + /** * Shows the scope's expand/collapse arrow. */ @@ -2232,8 +2183,9 @@ Variable.prototype = Heritage.extend(Scope.prototype, { // Sort all of the properties before adding them, if preferred. if (aOptions.sorted) { - propertyNames.sort(); + propertyNames.sort(this._naturalSort); } + // Add all the variable properties. for (let name of propertyNames) { let descriptor = Object.getOwnPropertyDescriptor(aObject, name); diff --git a/toolkit/devtools/shared/widgets/VariablesViewController.jsm b/toolkit/devtools/shared/widgets/VariablesViewController.jsm index d7de598fb6..f533f99d67 100644 --- a/toolkit/devtools/shared/widgets/VariablesViewController.jsm +++ b/toolkit/devtools/shared/widgets/VariablesViewController.jsm @@ -32,8 +32,11 @@ XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm"); const MAX_LONG_STRING_LENGTH = 200000; +const MAX_PROPERTY_ITEMS = 2000; const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties"; +const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data + this.EXPORTED_SYMBOLS = ["VariablesViewController", "StackFrameUtils"]; @@ -158,6 +161,166 @@ VariablesViewController.prototype = { return deferred.promise; }, + /** + * Adds pseudo items in case there is too many properties to display. + * Each item can expand into property slices. + * + * @param Scope aTarget + * The Scope where the properties will be placed into. + * @param object aGrip + * The property iterator grip. + * @param object aIterator + * The property iterator client. + */ + _populatePropertySlices: function(aTarget, aGrip, aIterator) { + if (aGrip.count < MAX_PROPERTY_ITEMS) { + return this._populateFromPropertyIterator(aTarget, aGrip); + } + + // Divide the keys into quarters. + let items = Math.ceil(aGrip.count / 4); + + let promises = []; + for(let i = 0; i < 4; i++) { + let start = aGrip.start + i * items; + let count = i != 3 ? items : aGrip.count - i * items; + + // Create a new kind of grip, with additional fields to define the slice + let sliceGrip = { + type: "property-iterator", + propertyIterator: aIterator, + start: start, + count: count + }; + + // Query the name of the first and last items for this slice + let deferred = promise.defer(); + aIterator.names([start, start + count - 1], ({ names }) => { + let label = "[" + names[0] + ELLIPSIS + names[1] + "]"; + let item = aTarget.addItem(label); + item.showArrow(); + this.addExpander(item, sliceGrip); + deferred.resolve(); + }); + promises.push(deferred.promise); + } + + return promise.all(promises); + }, + + /** + * Adds a property slice for a Variable in the view using the already + * property iterator + * + * @param Scope aTarget + * The Scope where the properties will be placed into. + * @param object aGrip + * The property iterator grip. + */ + _populateFromPropertyIterator: function(aTarget, aGrip) { + if (aGrip.count >= MAX_PROPERTY_ITEMS) { + // We already started to split, but there is still too many properties, split again. + return this._populatePropertySlices(aTarget, aGrip, aGrip.propertyIterator); + } + // We started slicing properties, and the slice is now small enough to be displayed + let deferred = promise.defer(); + aGrip.propertyIterator.slice(aGrip.start, aGrip.count, + ({ ownProperties }) => { + // Add all the variable properties. + if (Object.keys(ownProperties).length > 0) { + aTarget.addItems(ownProperties, { + sorted: true, + // Expansion handlers must be set after the properties are added. + callback: this.addExpander + }); + } + deferred.resolve(); + }); + return deferred.promise; + }, + + /** + * Adds the properties for a Variable in the view using a new feature in FF40+ + * that allows iteration over properties in slices. + * + * @param Scope aTarget + * The Scope where the properties will be placed into. + * @param object aGrip + * The grip to use to populate the target. + * @param string aQuery [optional] + * The query string used to fetch only a subset of properties + */ + _populateFromObjectWithIterator: function(aTarget, aGrip, aQuery) { + // FF40+ starts exposing `ownPropertyLength` on ObjectActor's grip, + // as well as `enumProperties` request. + let deferred = promise.defer(); + let objectClient = this._getObjectClient(aGrip); + let isArray = aGrip.preview && aGrip.preview.kind === "ArrayLike"; + if (isArray) { + // First enumerate array items, e.g. properties from `0` to `array.length`. + let options = { + ignoreNonIndexedProperties: true, + ignoreSafeGetters: true, + query: aQuery + }; + objectClient.enumProperties(options, ({ iterator }) => { + let sliceGrip = { + type: "property-iterator", + propertyIterator: iterator, + start: 0, + count: iterator.count + }; + this._populatePropertySlices(aTarget, sliceGrip, iterator) + .then(() => { + // Then enumerate the rest of the properties, like length, buffer, etc. + let options = { + ignoreIndexedProperties: true, + sort: true, + query: aQuery + }; + objectClient.enumProperties(options, ({ iterator }) => { + let sliceGrip = { + type: "property-iterator", + propertyIterator: iterator, + start: 0, + count: iterator.count + }; + deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip, iterator)); + }); + }); + }); + } else { + // For objects, we just enumerate all the properties sorted by name. + objectClient.enumProperties({ sort: true, query: aQuery }, ({ iterator }) => { + let sliceGrip = { + type: "property-iterator", + propertyIterator: iterator, + start: 0, + count: iterator.count + }; + deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip, iterator)); + }); + + } + return deferred.promise; + }, + + /** + * Adds the given prototype in the view. + * + * @param Scope aTarget + * The Scope where the properties will be placed into. + * @param object aProtype + * The prototype grip. + */ + _populateObjectPrototype: function(aTarget, aPrototype) { + // Add the variable's __proto__. + if (aPrototype && aPrototype.type != "null") { + let proto = aTarget.addItem("__proto__", { value: aPrototype }); + this.addExpander(proto, aPrototype); + } + }, + /** * Adds properties to a Scope, Variable, or Property in the view. Triggered * when a scope is expanded or certain variables are hovered. @@ -168,7 +331,19 @@ VariablesViewController.prototype = { * The grip to use to populate the target. */ _populateFromObject: function(aTarget, aGrip) { - let deferred = promise.defer(); + // Fetch properties by slices if there is too many in order to prevent UI freeze. + if ("ownPropertyLength" in aGrip && aGrip.ownPropertyLength >= MAX_PROPERTY_ITEMS) { + return this._populateFromObjectWithIterator(aTarget, aGrip) + .then(() => { + let deferred = promise.defer(); + let objectClient = this._getObjectClient(aGrip); + objectClient.getPrototype(({ prototype }) => { + this._populateObjectPrototype(aTarget, prototype); + deferred.resolve(); + }); + return deferred.promise; + }); + } if (aGrip.class === "Promise" && aGrip.promiseState) { const { state, value, reason } = aGrip.promiseState; @@ -179,10 +354,16 @@ VariablesViewController.prototype = { this.addExpander(aTarget.addItem("", { value: reason }), reason); } } + return this._populateProperties(aTarget, aGrip); + }, + + _populateProperties: function(aTarget, aGrip, aOptions) { + let deferred = promise.defer(); let objectClient = this._getObjectClient(aGrip); objectClient.getPrototypeAndProperties(aResponse => { - let { ownProperties, prototype } = aResponse; + let ownProperties = aResponse.ownProperties || {}; + let prototype = aResponse.prototype || null; // 'safeGetterValues' is new and isn't necessary defined on old actors. let safeGetterValues = aResponse.safeGetterValues || {}; let sortable = VariablesView.isSortable(aGrip.class); @@ -200,21 +381,15 @@ VariablesViewController.prototype = { } // Add all the variable properties. - if (ownProperties) { - aTarget.addItems(ownProperties, { - // Not all variables need to force sorted properties. - sorted: sortable, - // Expansion handlers must be set after the properties are added. - callback: this.addExpander - }); - } + aTarget.addItems(ownProperties, { + // Not all variables need to force sorted properties. + sorted: sortable, + // Expansion handlers must be set after the properties are added. + callback: this.addExpander + }); // Add the variable's __proto__. - if (prototype && prototype.type != "null") { - let proto = aTarget.addItem("__proto__", { value: prototype }); - // Expansion handlers must be set after the properties are added. - this.addExpander(proto, prototype); - } + this._populateObjectPrototype(aTarget, prototype); // If the object is a function we need to fetch its scope chain // to show them as closures for the respective function. @@ -389,6 +564,10 @@ VariablesViewController.prototype = { let deferred = promise.defer(); aTarget._fetched = deferred.promise; + if (aSource.type === "property-iterator") { + return this._populateFromPropertyIterator(aTarget, aSource); + } + // If the target is a Variable or Property then we're fetching properties. if (VariablesView.isVariable(aTarget)) { this._populateFromObject(aTarget, aSource).then(() => { @@ -438,6 +617,29 @@ VariablesViewController.prototype = { return deferred.promise; }, + /** + * Indicates to the view if the targeted actor supports properties search + * + * @return boolean True, if the actor supports enumProperty request + */ + supportsSearch: function () { + // FF40+ starts exposing ownPropertyLength on object actor's grip + // as well as enumProperty which allows to query a subset of properties. + return this.objectActor && ("ownPropertyLength" in this.objectActor); + }, + + /** + * Try to use the actor to perform an attribute search. + * + * @param Scope aScope + * The Scope instance to populate with properties + * @param string aToken + * The query string + */ + performSearch: function(aScope, aToken) { + this._populateFromObjectWithIterator(aScope, this.objectActor, aToken); + }, + /** * Release an actor from the controller. * @@ -497,6 +699,8 @@ VariablesViewController.prototype = { let populated; if (aOptions.objectActor) { + // Save objectActor for properties filtering + this.objectActor = aOptions.objectActor; populated = this.populate(variable, aOptions.objectActor); variable.expand(); } else if (aOptions.rawObject) { diff --git a/toolkit/devtools/webconsole/console-output.js b/toolkit/devtools/webconsole/console-output.js index db5aa1dda1..32ddcf506e 100644 --- a/toolkit/devtools/webconsole/console-output.js +++ b/toolkit/devtools/webconsole/console-output.js @@ -341,6 +341,10 @@ ConsoleOutput.prototype = { this.owner.owner.openLink.apply(this.owner.owner, arguments); }, + openLocationInDebugger: function ({url, line}) { + return this.owner.owner.viewSourceInDebugger(url, line); + }, + /** * Open the variables view to inspect an object actor. * @see JSTerm.openVariablesView() in webconsole.js @@ -2476,6 +2480,8 @@ Widgets.JSObject.prototype = Heritage.extend(Widgets.BaseWidget.prototype, options.onClick = options.href ? this._onClickAnchor : this._onClick; } + options.onContextMenu = options.onContextMenu || this._onContextMenu; + let anchor = this.el("a", { class: options.className, draggable: false, @@ -2484,6 +2490,8 @@ Widgets.JSObject.prototype = Heritage.extend(Widgets.BaseWidget.prototype, this.message._addLinkCallback(anchor, options.onClick); + anchor.addEventListener("contextmenu", options.onContextMenu.bind(this)); + if (options.appendTo) { options.appendTo.appendChild(anchor); } else if (!("appendTo" in options) && this.element) { @@ -2493,16 +2501,38 @@ Widgets.JSObject.prototype = Heritage.extend(Widgets.BaseWidget.prototype, return anchor; }, + openObjectInVariablesView: function() + { + this.output.openVariablesView({ + label: VariablesView.getString(this.objectActor, { concise: true }), + objectActor: this.objectActor, + autofocus: true, + }); + }, + /** * The click event handler for objects shown inline. * @private */ _onClick: function() { - this.output.openVariablesView({ - label: VariablesView.getString(this.objectActor, { concise: true }), - objectActor: this.objectActor, - autofocus: true, + this.openObjectInVariablesView(); + }, + + _onContextMenu: function(ev) { + // TODO offer a nice API for the context menu. + // Probably worth to take a look at Firebug's way + // https://github.com/firebug/firebug/blob/master/extension/content/firebug/chrome/menu.js + let doc = ev.target.ownerDocument; + let cmPopup = doc.getElementById("output-contextmenu"); + let openInVarViewCmd = doc.getElementById("menu_openInVarView"); + let openVarView = this.openObjectInVariablesView.bind(this); + openInVarViewCmd.addEventListener("command", openVarView); + openInVarViewCmd.removeAttribute("disabled"); + cmPopup.addEventListener("popuphiding", function onPopupHiding() { + cmPopup.removeEventListener("popuphiding", onPopupHiding); + openInVarViewCmd.removeEventListener("command", openVarView); + openInVarViewCmd.setAttribute("disabled", "true"); }); }, @@ -2670,6 +2700,16 @@ Widgets.ObjectRenderers.add({ this._text(")"); }, + + _onClick: function () { + let location = this.objectActor.location; + if (location) { + this.output.openLocationInDebugger(location); + } + else { + this.openObjectInVariablesView(); + } + } }); // Widgets.ObjectRenderers.byClass.Function /** diff --git a/toolkit/devtools/webconsole/test/browser.ini b/toolkit/devtools/webconsole/test/browser.ini new file mode 100644 index 0000000000..585dcc38f7 --- /dev/null +++ b/toolkit/devtools/webconsole/test/browser.ini @@ -0,0 +1,393 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + head.js + test-bug-585956-console-trace.html + test-bug-593003-iframe-wrong-hud-iframe.html + test-bug-593003-iframe-wrong-hud.html + test-bug-595934-canvas-css.html + test-bug-595934-canvas-css.js + test-bug-595934-css-loader.css + test-bug-595934-css-loader.css^headers^ + test-bug-595934-css-loader.html + test-bug-595934-css-parser.css + test-bug-595934-css-parser.html + test-bug-595934-empty-getelementbyid.html + test-bug-595934-empty-getelementbyid.js + test-bug-595934-html.html + test-bug-595934-image.html + test-bug-595934-image.jpg + test-bug-595934-imagemap.html + test-bug-595934-malformedxml-external.html + test-bug-595934-malformedxml-external.xml + test-bug-595934-malformedxml.xhtml + test-bug-595934-svg.xhtml + test-bug-595934-workers.html + test-bug-595934-workers.js + test-bug-597136-external-script-errors.html + test-bug-597136-external-script-errors.js + test-bug-597756-reopen-closed-tab.html + test-bug-599725-response-headers.sjs + test-bug-600183-charset.html + test-bug-600183-charset.html^headers^ + test-bug-601177-log-levels.html + test-bug-601177-log-levels.js + test-bug-603750-websocket.html + test-bug-603750-websocket.js + test-bug-613013-console-api-iframe.html + test-bug-618078-network-exceptions.html + test-bug-621644-jsterm-dollar.html + test-bug-630733-response-redirect-headers.sjs + test-bug-632275-getters.html + test-bug-632347-iterators-generators.html + test-bug-644419-log-limits.html + test-bug-646025-console-file-location.html + test-bug-658368-time-methods.html + test-bug-737873-mixedcontent.html + test-bug-752559-ineffective-iframe-sandbox-warning0.html + test-bug-752559-ineffective-iframe-sandbox-warning1.html + test-bug-752559-ineffective-iframe-sandbox-warning2.html + test-bug-752559-ineffective-iframe-sandbox-warning3.html + test-bug-752559-ineffective-iframe-sandbox-warning4.html + test-bug-752559-ineffective-iframe-sandbox-warning5.html + test-bug-752559-ineffective-iframe-sandbox-warning-inner.html + test-bug-752559-ineffective-iframe-sandbox-warning-nested1.html + test-bug-752559-ineffective-iframe-sandbox-warning-nested2.html + test-bug-762593-insecure-passwords-about-blank-web-console-warning.html + test-bug-762593-insecure-passwords-web-console-warning.html + test-bug-766001-console-log.js + test-bug-766001-js-console-links.html + test-bug-766001-js-errors.js + test-bug-782653-css-errors-1.css + test-bug-782653-css-errors-2.css + test-bug-782653-css-errors.html + test-bug-837351-security-errors.html + test-bug-846918-hsts-invalid-headers.html + test-bug-846918-hsts-invalid-headers.html^headers^ + test-bug-859170-longstring-hang.html + test-bug-869003-iframe.html + test-bug-869003-top-window.html + test-closure-optimized-out.html + test-closures.html + test-console-assert.html + test-console-count.html + test-console-count-external-file.js + test-console-extras.html + test-console-replaced-api.html + test-console.html + test-console-workers.html + test-console-table.html + test-console-output-02.html + test-console-output-03.html + test-console-output-04.html + test-console-output-dom-elements.html + test-console-output-events.html + test-console-output-regexp.html + test-console-column.html + test-consoleiframes.html + test-certificate-messages.html + test-data.json + test-data.json^headers^ + test-duplicate-error.html + test-encoding-ISO-8859-1.html + test-error.html + test-eval-in-stackframe.html + test-file-location.js + test-filter.html + test-for-of.html + test-iframe-762593-insecure-form-action.html + test-iframe-762593-insecure-frame.html + test-iframe1.html + test-iframe2.html + test-iframe3.html + test-image.png + test-mixedcontent-securityerrors.html + test-mutation.html + test-network-request.html + test-network.html + test-observe-http-ajax.html + test-own-console.html + test-property-provider.html + test-repeated-messages.html + test-result-format-as-string.html + test-webconsole-error-observer.html + test_bug_770099_violation.html + test_bug_770099_violation.html^headers^ + test-autocomplete-in-stackframe.html + testscript.js + test-bug_923281_console_log_filter.html + test-bug_923281_test1.js + test-bug_923281_test2.js + test-bug_939783_console_trace_duplicates.html + test-bug-952277-highlight-nodes-in-vview.html + test-bug-609872-cd-iframe-parent.html + test-bug-609872-cd-iframe-child.html + test-bug-989025-iframe-parent.html + test-bug_1050691_click_function_to_source.html + test-bug_1050691_click_function_to_source.js + test-console-api-stackframe.html + test_bug_1010953_cspro.html^headers^ + test_bug_1010953_cspro.html + test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^ + test_bug1045902_console_csp_ignore_reflected_xss_message.html + test_bug1092055_shouldwarn.js^headers^ + test_bug1092055_shouldwarn.js + test_bug1092055_shouldwarn.html + +[browser_bug1045902_console_csp_ignore_reflected_xss_message.js] +[browser_bug664688_sandbox_update_after_navigation.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (intermittent Linux debug) +[browser_bug_638949_copy_link_location.js] +[browser_bug_862916_console_dir_and_filter_off.js] +[browser_bug_865288_repeat_different_objects.js] +[browser_bug_865871_variables_view_close_on_esc_key.js] +[browser_bug_869003_inspect_cross_domain_object.js] +[browser_bug_871156_ctrlw_close_tab.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (intermittent Linux debug) +[browser_cached_messages.js] +skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests (expectUncaughtException) +[browser_console.js] +[browser_console_addonsdk_loader_exception.js] +[browser_console_clear_on_reload.js] +[browser_console_click_focus.js] +[browser_console_consolejsm_output.js] +[browser_console_copy_command.js] +[browser_console_dead_objects.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_console_copy_entire_message_context_menu.js] +[browser_console_error_source_click.js] +skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests +[browser_console_filters.js] +[browser_console_iframe_messages.js] +skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests +[browser_console_keyboard_accessibility.js] +[browser_console_log_inspectable_object.js] +[browser_console_native_getters.js] +[browser_console_navigation_marker.js] +[browser_console_nsiconsolemessage.js] +skip-if = buildapp == 'mulet' +[browser_console_optimized_out_vars.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_console_private_browsing.js] +skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests +[browser_console_variables_view.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_console_variables_view_dom_nodes.js] +[browser_console_variables_view_dont_sort_non_sortable_classes_properties.js] +skip-if = buildapp == 'mulet' +[browser_console_variables_view_while_debugging.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_console_variables_view_while_debugging_and_inspecting.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_eval_in_debugger_stackframe.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_eval_in_debugger_stackframe2.js] +[browser_jsterm_inspect.js] +[browser_longstring_hang.js] +[browser_netpanel_longstring_expand.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_output_breaks_after_console_dir_uninspectable.js] +[browser_output_longstring_expand.js] +[browser_repeated_messages_accuracy.js] +skip-if = buildapp == 'mulet' +[browser_result_format_as_string.js] +[browser_warn_user_about_replaced_api.js] +[browser_webconsole_abbreviate_source_url.js] +[browser_webconsole_allow_mixedcontent_securityerrors.js] +skip-if = buildapp == 'mulet' +[browser_webconsole_assert.js] +[browser_webconsole_basic_net_logging.js] +[browser_webconsole_block_mixedcontent_securityerrors.js] +skip-if = buildapp == 'mulet' +[browser_webconsole_bug_579412_input_focus.js] +[browser_webconsole_bug_580001_closing_after_completion.js] +[browser_webconsole_bug_580030_errors_after_page_reload.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_580454_timestamp_l10n.js] +[browser_webconsole_bug_582201_duplicate_errors.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_583816_No_input_and_Tab_key_pressed.js] +[browser_webconsole_bug_585237_line_limit.js] +[browser_webconsole_bug_585956_console_trace.js] +[browser_webconsole_bug_585991_autocomplete_keys.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_585991_autocomplete_popup.js] +[browser_webconsole_bug_586388_select_all.js] +[browser_webconsole_bug_587617_output_copy.js] +[browser_webconsole_bug_588342_document_focus.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_588730_text_node_insertion.js] +[browser_webconsole_bug_588967_input_expansion.js] +[browser_webconsole_bug_589162_css_filter.js] +[browser_webconsole_bug_592442_closing_brackets.js] +[browser_webconsole_bug_593003_iframe_wrong_hud.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_594477_clickable_output.js] +[browser_webconsole_bug_594497_history_arrow_keys.js] +[browser_webconsole_bug_595223_file_uri.js] +[browser_webconsole_bug_595350_multiple_windows_and_tabs.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_595934_message_categories.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_597136_external_script_errors.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_597136_network_requests_from_chrome.js] +[browser_webconsole_bug_597460_filter_scroll.js] +[browser_webconsole_bug_597756_reopen_closed_tab.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_599725_response_headers.js] +[browser_webconsole_bug_600183_charset.js] +[browser_webconsole_bug_601177_log_levels.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_601352_scroll.js] +[browser_webconsole_bug_601667_filter_buttons.js] +[browser_webconsole_bug_602572_log_bodies_checkbox.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_603750_websocket.js] +[browser_webconsole_bug_611795.js] +[browser_webconsole_bug_613013_console_api_iframe.js] +[browser_webconsole_bug_613280_jsterm_copy.js] +[browser_webconsole_bug_613642_maintain_scroll.js] +[browser_webconsole_bug_613642_prune_scroll.js] +[browser_webconsole_bug_614793_jsterm_scroll.js] +[browser_webconsole_bug_618078_network_exceptions.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_618311_close_panels.js] +[browser_webconsole_bug_621644_jsterm_dollar.js] +[browser_webconsole_bug_622303_persistent_filters.js] +[browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js] +skip-if = os != "win" +[browser_webconsole_bug_630733_response_redirect_headers.js] +[browser_webconsole_bug_632275_getters_document_width.js] +[browser_webconsole_bug_632347_iterators_generators.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_632817.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_642108_pruneTest.js] +[browser_webconsole_autocomplete_and_selfxss.js] +[browser_webconsole_bug_644419_log_limits.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_bug_646025_console_file_location.js] +[browser_webconsole_bug_651501_document_body_autocomplete.js] +[browser_webconsole_bug_653531_highlighter_console_helper.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_658368_time_methods.js] +[browser_webconsole_bug_659907_console_dir.js] +[browser_webconsole_bug_660806_history_nav.js] +[browser_webconsole_bug_664131_console_group.js] +[browser_webconsole_bug_686937_autocomplete_JSTerm_helpers.js] +[browser_webconsole_bug_704295.js] +[browser_webconsole_bug_734061_No_input_change_and_Tab_key_pressed.js] +[browser_webconsole_bug_737873_mixedcontent.js] +[browser_webconsole_bug_752559_ineffective_iframe_sandbox_warning.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout) +[browser_webconsole_bug_762593_insecure_passwords_about_blank_web_console_warning.js] +skip-if = buildapp == 'mulet' +[browser_webconsole_bug_762593_insecure_passwords_web_console_warning.js] +skip-if = true # Bug 1110500 - mouse event failure in test +[browser_webconsole_bug_764572_output_open_url.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_bug_766001_JS_Console_in_Debugger.js] +skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests (expectUncaughtException) +[browser_webconsole_bug_770099_violation.js] +[browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js] +skip-if = buildapp == 'mulet' +[browser_webconsole_bug_804845_ctrl_key_nav.js] +skip-if = os != "mac" +[browser_webconsole_bug_817834_add_edited_input_to_history.js] +[browser_webconsole_bug_837351_securityerrors.js] +skip-if = buildapp == 'mulet' +[browser_webconsole_bug_846918_hsts_invalid-headers.js] +skip-if = buildapp == 'mulet' || e10s # Bug 1042253 - webconsole e10s tests +[browser_webconsole_bug_915141_toggle_response_logging_with_keyboard.js] +[browser_webconsole_filter_buttons_contextmenu.js] +[browser_webconsole_bug_1006027_message_timestamps_incorrect.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug intermittent) +[browser_webconsole_bug_1010953_cspro.js] +[browser_webconsole_certificate_messages.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_show_subresource_security_errors.js] +[browser_webconsole_cached_autocomplete.js] +[browser_webconsole_change_font_size.js] +[browser_webconsole_chrome.js] +[browser_webconsole_clickable_urls.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout) +[browser_webconsole_closure_inspection.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_completion.js] +[browser_webconsole_console_extras.js] +[browser_webconsole_console_logging_api.js] +[browser_webconsole_console_logging_workers_api.js] +[browser_webconsole_count.js] +[browser_webconsole_dont_navigate_on_doubleclick.js] +[browser_webconsole_execution_scope.js] +[browser_webconsole_for_of.js] +[browser_webconsole_history.js] +[browser_webconsole_input_field_focus_on_panel_select.js] +[browser_webconsole_inspect-parsed-documents.js] +[browser_webconsole_js_input_expansion.js] +[browser_webconsole_jsterm.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout) +[browser_webconsole_live_filtering_of_message_types.js] +[browser_webconsole_live_filtering_on_search_strings.js] +[browser_webconsole_message_node_id.js] +[browser_webconsole_netlogging.js] +[browser_webconsole_network_panel.js] +[browser_webconsole_notifications.js] +[browser_webconsole_open-links-without-callback.js] +[browser_webconsole_promise.js] +[browser_webconsole_output_copy_newlines.js] +[browser_webconsole_output_order.js] +[browser_webconsole_property_provider.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_scratchpad_panel_link.js] +[browser_webconsole_split.js] +[browser_webconsole_split_escape_key.js] +[browser_webconsole_split_focus.js] +[browser_webconsole_split_persist.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout) +[browser_webconsole_view_source.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s (expectUncaughtException) +[browser_webconsole_reflow.js] +[browser_webconsole_log_file_filter.js] +[browser_webconsole_expandable_timestamps.js] +[browser_webconsole_autocomplete_in_debugger_stackframe.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_autocomplete_popup_close_on_tab_switch.js] +skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s +[browser_webconsole_autocomplete-properties-with-non-alphanumeric-names.js] +[browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js] +[browser_console_history_persist.js] +[browser_webconsole_output_01.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests +[browser_webconsole_output_02.js] +[browser_webconsole_output_03.js] +[browser_webconsole_output_04.js] +[browser_webconsole_output_05.js] +[browser_webconsole_output_06.js] +[browser_webconsole_output_dom_elements_01.js] +[browser_webconsole_output_dom_elements_02.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout) +[browser_webconsole_output_dom_elements_03.js] +[browser_webconsole_output_dom_elements_04.js] +skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout) +[browser_webconsole_output_events.js] +[browser_webconsole_output_regexp.js] +[browser_webconsole_output_table.js] +[browser_console_variables_view_highlighter.js] +[browser_webconsole_start_netmon_first.js] +[browser_webconsole_console_trace_duplicates.js] +[browser_webconsole_cd_iframe.js] +[browser_webconsole_autocomplete_crossdomain_iframe.js] +[browser_webconsole_console_custom_styles.js] +[browser_webconsole_console_api_stackframe.js] +[browser_webconsole_column_numbers.js] +[browser_console_open_or_focus.js] +[browser_webconsole_bug_922212_console_dirxml.js] +[browser_webconsole_shows_reqs_in_netmonitor.js] +[browser_netmonitor_shows_reqs_in_webconsole.js] +[browser_webconsole_bug_1050691_click_function_to_source.js] +[browser_webconsole_context_menu_open_in_var_view.js] diff --git a/toolkit/devtools/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js b/toolkit/devtools/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js new file mode 100644 index 0000000000..5cc65e7507 --- /dev/null +++ b/toolkit/devtools/webconsole/test/browser_console_variables_view_dont_sort_non_sortable_classes_properties.js @@ -0,0 +1,132 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/* Test case that ensures Array and other list types are not sorted in variables + * view. + * + * The tested types are: + * - Array + * - Int8Array + * - Int16Array + * - Int32Array + * - Uint8Array + * - Uint16Array + * - Uint32Array + * - Uint8ClampedArray + * - Float32Array + * - Float64Array + * - NodeList + */ + +function test() { + const TEST_URI = "data:text/html;charset=utf-8, \ + \ + \ + Test document for bug 977500 \ + \ + \ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ + \ + "; + + let jsterm; + + function* runner() { + const typedArrayTypes = ["Int8Array", "Int16Array", "Int32Array", + "Uint8Array", "Uint16Array", "Uint32Array", + "Uint8ClampedArray", "Float32Array", + "Float64Array"]; + + const {tab} = yield loadTab(TEST_URI); + const hud = yield openConsole(tab); + jsterm = hud.jsterm; + + // Create an ArrayBuffer of 80 bytes to test TypedArrays. 80 bytes is + // enough to get 10 items in all different TypedArrays. + yield jsterm.execute("let buf = new ArrayBuffer(80);"); + + // Array + yield testNotSorted("Array(0,1,2,3,4,5,6,7,8,9,10)"); + // NodeList + yield testNotSorted("document.querySelectorAll('div')"); + // Object + yield testSorted("Object({'hello':1,1:5,10:2,4:2,'abc':1})"); + + // Typed arrays. + for (let type of typedArrayTypes) { + yield testNotSorted("new " + type + "(buf)"); + } + } + + /** + * A helper that ensures the properties are not sorted when an object + * specified by aObject is inspected. + * + * @param string aObject + * A string that, once executed, creates and returns the object to + * inspect. + */ + function testNotSorted(aObject) { + info("Testing " + aObject); + let deferred = promise.defer(); + jsterm.once("variablesview-fetched", (_, aVar) => deferred.resolve(aVar)); + jsterm.execute("inspect(" + aObject + ")"); + + let variableScope = yield deferred.promise; + ok(variableScope, "Variables view opened"); + + // If the properties are sorted: keys = ["0", "1", "10",...] <- incorrect + // If the properties are not sorted: keys = ["0", "1", "2",...] <- correct + let keyIterator = variableScope._store.keys(); + is(keyIterator.next().value, "0", "First key is 0"); + is(keyIterator.next().value, "1", "Second key is 1"); + + // If the properties are sorted, the next one will be 10. + is(keyIterator.next().value, "2", "Third key is 2, not 10"); + } + /** + * A helper that ensures the properties are sorted when an object + * specified by aObject is inspected. + * + * @param string aObject + * A string that, once executed, creates and returns the object to + * inspect. + */ + function testSorted(aObject) { + info("Testing " + aObject); + let deferred = promise.defer(); + jsterm.once("variablesview-fetched", (_, aVar) => deferred.resolve(aVar)); + jsterm.execute("inspect(" + aObject + ")"); + + let variableScope = yield deferred.promise; + ok(variableScope, "Variables view opened"); + + // If the properties are sorted: keys = ["1", "4", "10",..., "abc", "hello"] <- correct + // If the properties are not sorted: keys = ["1", "10", "4",...] <- incorrect + let keyIterator = variableScope._store.keys(); + is(keyIterator.next().value, "1", "First key should be 1"); + is(keyIterator.next().value, "4", "Second key should be 4"); + + // If the properties are sorted, the next one will be 10. + is(keyIterator.next().value, "10", "Third key is 10"); + // If sorted next properties should be "abc" then "hello" + is(keyIterator.next().value, "abc", "Fourth key is abc"); + is(keyIterator.next().value, "hello", "Fifth key is hello"); + } + + Task.spawn(runner).then(finishTest); +} diff --git a/toolkit/devtools/webconsole/test/browser_webconsole_bug_1050691_click_function_to_source.js b/toolkit/devtools/webconsole/test/browser_webconsole_bug_1050691_click_function_to_source.js new file mode 100644 index 0000000000..528bcdccf7 --- /dev/null +++ b/toolkit/devtools/webconsole/test/browser_webconsole_bug_1050691_click_function_to_source.js @@ -0,0 +1,60 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// Tests that clicking on a function displays its source in the debugger. + +"use strict"; + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug_1050691_click_function_to_source.html"; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + yield testWithoutDebuggerOpen(hud); + + // Open the Debugger panel. + let debuggerPanel = yield openDebugger(); + // And right after come back to the Console panel. + yield openConsole(); + yield testWithDebuggerOpen(hud, debuggerPanel); +}); + +function* testWithoutDebuggerOpen(hud) { + let clickable = yield printFunction(hud); + let onVariablesViewOpen = hud.jsterm.once("variablesview-fetched"); + synthesizeClick(clickable, hud); + return onVariablesViewOpen; +} + +function* testWithDebuggerOpen(hud, debuggerPanel) { + let clickable = yield printFunction(hud); + let panelWin = debuggerPanel.panelWin; + let onEditorLocationSet = panelWin.once(panelWin.EVENTS.EDITOR_LOCATION_SET); + synthesizeClick(clickable, hud); + yield onEditorLocationSet; + ok(isDebuggerCaretPos(debuggerPanel, 7), + "Clicking on a function should go to its source in the debugger view"); +} + +function synthesizeClick(clickable, hud) { + EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow); +} + +let printFunction = Task.async(function* (hud) { + hud.jsterm.clearOutput(); + content.wrappedJSObject.foo(); + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + let msg = [...result.matched][0]; + let clickable = msg.querySelector("a"); + ok(clickable, "clickable item for object should exist"); + return clickable; +}); diff --git a/toolkit/devtools/webconsole/test/browser_webconsole_closure_inspection.js b/toolkit/devtools/webconsole/test/browser_webconsole_closure_inspection.js new file mode 100644 index 0000000000..f5eea6b166 --- /dev/null +++ b/toolkit/devtools/webconsole/test/browser_webconsole_closure_inspection.js @@ -0,0 +1,97 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Check that inspecting a closure in the variables view sidebar works when +// execution is paused. + +const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-closures.html"; + +let gWebConsole, gJSTerm, gVariablesView; + +function test() +{ + registerCleanupFunction(() => { + gWebConsole = gJSTerm = gVariablesView = null; + }); + + loadTab(TEST_URI).then(() => { + openConsole().then((hud) => { + openDebugger().then(({ toolbox, panelWin }) => { + let deferred = promise.defer(); + panelWin.gThreadClient.addOneTimeListener("resumed", (aEvent, aPacket) => { + ok(true, "Debugger resumed"); + deferred.resolve({ toolbox: toolbox, panelWin: panelWin }); + }); + return deferred.promise; + }).then(({ toolbox, panelWin }) => { + let deferred = promise.defer(); + panelWin.once(panelWin.EVENTS.FETCHED_SCOPES, (aEvent, aPacket) => { + ok(true, "Scopes were fetched"); + toolbox.selectTool("webconsole").then(() => consoleOpened(hud)); + deferred.resolve(); + }); + + let button = content.document.querySelector("button"); + ok(button, "button element found"); + EventUtils.synthesizeMouseAtCenter(button, {}, content); + + return deferred.promise; + }); + }); + }); +} + +function consoleOpened(hud) +{ + gWebConsole = hud; + gJSTerm = hud.jsterm; + gJSTerm.execute("window.george.getName"); + + waitForMessages({ + webconsole: gWebConsole, + messages: [{ + text: "function _pfactory/<.getName()", + category: CATEGORY_OUTPUT, + objects: true, + }], + }).then(onExecuteGetName); +} + +function onExecuteGetName(aResults) +{ + let clickable = aResults[0].clickableElements[0]; + ok(clickable, "clickable object found"); + + gJSTerm.once("variablesview-fetched", onGetNameFetch); + let contextMenu = + gWebConsole.iframeWindow.document.getElementById("output-contextmenu"); + waitForContextMenu(contextMenu, clickable, () => { + let openInVarView = contextMenu.querySelector("#menu_openInVarView"); + ok(openInVarView.disabled === false, + "the \"Open In Variables View\" context menu item should be clickable"); + // EventUtils.synthesizeMouseAtCenter seems to fail here in Mac OSX + openInVarView.click(); + }); +} + +function onGetNameFetch(aEvent, aVar) +{ + gVariablesView = aVar._variablesView; + ok(gVariablesView, "variables view object"); + + findVariableViewProperties(aVar, [ + { name: /_pfactory/, value: "" }, + ], { webconsole: gWebConsole }).then(onExpandClosure); +} + +function onExpandClosure(aResults) +{ + let prop = aResults[0].matchedProp; + ok(prop, "matched the name property in the variables view"); + + gVariablesView.window.focus(); + gJSTerm.once("sidebar-closed", finishTest); + EventUtils.synthesizeKey("VK_ESCAPE", {}); +} diff --git a/toolkit/devtools/webconsole/test/browser_webconsole_context_menu_open_in_var_view.js b/toolkit/devtools/webconsole/test/browser_webconsole_context_menu_open_in_var_view.js new file mode 100644 index 0000000000..e169d61e1e --- /dev/null +++ b/toolkit/devtools/webconsole/test/browser_webconsole_context_menu_open_in_var_view.js @@ -0,0 +1,51 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// Tests that the "Open in Variables View" context menu item is enabled +// only for objects. + +"use strict"; + +const TEST_URI = `data:text/html,`; + +let test = asyncTest(function*() { + yield loadTab(TEST_URI); + let hud = yield openConsole(); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + count: 2, + text: /foo/ + }], + }); + + let [msgWithText, msgWithObj] = [...result.matched]; + ok(msgWithText && msgWithObj, "Two messages should have appeared"); + + let contextMenu = hud.iframeWindow. + document.getElementById("output-contextmenu"); + let openInVarViewItem = contextMenu.querySelector("#menu_openInVarView"); + let obj = msgWithObj.querySelector(".cm-variable"); + let text = msgWithText.querySelector(".console-string"); + + yield waitForContextMenu(contextMenu, obj, () => { + ok(openInVarViewItem.disabled === false, "The \"Open In Variables View\" " + + "context menu item should be available for objects"); + }, () => { + ok(openInVarViewItem.disabled === true, "The \"Open In Variables View\" " + + "context menu item should be disabled on popup hiding"); + }); + + yield waitForContextMenu(contextMenu, text, () => { + ok(openInVarViewItem.disabled === true, "The \"Open In Variables View\" " + + "context menu item should be disabled for texts"); + }); +}); diff --git a/toolkit/devtools/webconsole/test/chrome.ini b/toolkit/devtools/webconsole/test/chrome.ini new file mode 100644 index 0000000000..e1efde569c --- /dev/null +++ b/toolkit/devtools/webconsole/test/chrome.ini @@ -0,0 +1,30 @@ +[DEFAULT] +skip-if = buildapp == 'b2g' +support-files = + common.js + data.json + data.json^headers^ + network_requests_iframe.html + sandboxed_iframe.html + +[test_basics.html] +[test_bug819670_getter_throws.html] +[test_cached_messages.html] +[test_consoleapi.html] +[test_consoleapi_innerID.html] +[test_file_uri.html] +[test_reflow.html] +[test_jsterm.html] +[test_jsterm_cd_iframe.html] +[test_jsterm_last_result.html] +[test_network_get.html] +[test_network_longstring.html] +[test_network_post.html] +[test_network_security-hpkp.html] +[test_network_security-hsts.html] +[test_nsiconsolemessage.html] +[test_object_actor.html] +[test_object_actor_native_getters.html] +[test_object_actor_native_getters_lenient_this.html] +[test_page_errors.html] +[test_throw.html] diff --git a/toolkit/devtools/webconsole/test/head.js b/toolkit/devtools/webconsole/test/head.js new file mode 100644 index 0000000000..f6893137e7 --- /dev/null +++ b/toolkit/devtools/webconsole/test/head.js @@ -0,0 +1,1734 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* 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"; + +let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); +let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {}); +let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); +let {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); +let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +let {require, TargetFactory} = devtools; +let {Utils: WebConsoleUtils} = require("devtools/toolkit/webconsole/utils"); +let {Messages} = require("devtools/webconsole/console-output"); +const asyncStorage = require("devtools/toolkit/shared/async-storage"); + +//Services.prefs.setBoolPref("devtools.debugger.log", true); + +let gPendingOutputTest = 0; + +// The various categories of messages. +const CATEGORY_NETWORK = 0; +const CATEGORY_CSS = 1; +const CATEGORY_JS = 2; +const CATEGORY_WEBDEV = 3; +const CATEGORY_INPUT = 4; +const CATEGORY_OUTPUT = 5; +const CATEGORY_SECURITY = 6; + +// The possible message severities. +const SEVERITY_ERROR = 0; +const SEVERITY_WARNING = 1; +const SEVERITY_INFO = 2; +const SEVERITY_LOG = 3; + +// The indent of a console group in pixels. +const GROUP_INDENT = 12; + +const WEBCONSOLE_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; +let WCU_l10n = new WebConsoleUtils.l10n(WEBCONSOLE_STRINGS_URI); + +gDevTools.testing = true; + +function asyncTest(generator) { + return () => { + Task.spawn(generator).then(finishTest); + }; +} + + +function loadTab(url) { + let deferred = promise.defer(); + + let tab = gBrowser.selectedTab = gBrowser.addTab(url); + let browser = gBrowser.getBrowserForTab(tab); + + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + deferred.resolve({tab: tab, browser: browser}); + }, true); + + return deferred.promise; +} + +function loadBrowser(browser) { + let deferred = promise.defer(); + + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + deferred.resolve(null); + }, true); + + return deferred.promise; +} + +function closeTab(tab) { + let deferred = promise.defer(); + + let container = gBrowser.tabContainer; + + container.addEventListener("TabClose", function onTabClose() { + container.removeEventListener("TabClose", onTabClose, true); + deferred.resolve(null); + }, true); + + gBrowser.removeTab(tab); + + return deferred.promise; +} + +function afterAllTabsLoaded(callback, win) { + win = win || window; + + let stillToLoad = 0; + + function onLoad() { + this.removeEventListener("load", onLoad, true); + stillToLoad--; + if (!stillToLoad) + callback(); + } + + for (let a = 0; a < win.gBrowser.tabs.length; a++) { + let browser = win.gBrowser.tabs[a].linkedBrowser; + if (browser.webProgress.isLoadingDocument) { + stillToLoad++; + browser.addEventListener("load", onLoad, true); + } + } + + if (!stillToLoad) + callback(); +} + +/** + * Check if a log entry exists in the HUD output node. + * + * @param {Element} aOutputNode + * the HUD output node. + * @param {string} aMatchString + * the string you want to check if it exists in the output node. + * @param {string} aMsg + * the message describing the test + * @param {boolean} [aOnlyVisible=false] + * find only messages that are visible, not hidden by the filter. + * @param {boolean} [aFailIfFound=false] + * fail the test if the string is found in the output node. + * @param {string} aClass [optional] + * find only messages with the given CSS class. + */ +function testLogEntry(aOutputNode, aMatchString, aMsg, aOnlyVisible, + aFailIfFound, aClass) +{ + let selector = ".message"; + // Skip entries that are hidden by the filter. + if (aOnlyVisible) { + selector += ":not(.filtered-by-type):not(.filtered-by-string)"; + } + if (aClass) { + selector += "." + aClass; + } + + let msgs = aOutputNode.querySelectorAll(selector); + let found = false; + for (let i = 0, n = msgs.length; i < n; i++) { + let message = msgs[i].textContent.indexOf(aMatchString); + if (message > -1) { + found = true; + break; + } + } + + is(found, !aFailIfFound, aMsg); +} + +/** + * A convenience method to call testLogEntry(). + * + * @param string aString + * The string to find. + */ +function findLogEntry(aString) +{ + testLogEntry(outputNode, aString, "found " + aString); +} + +/** + * Open the Web Console for the given tab. + * + * @param nsIDOMElement [aTab] + * Optional tab element for which you want open the Web Console. The + * default tab is taken from the global variable |tab|. + * @param function [aCallback] + * Optional function to invoke after the Web Console completes + * initialization (web-console-created). + * @return object + * A promise that is resolved once the web console is open. + */ +let openConsole = function(aTab) { + let webconsoleOpened = promise.defer(); + let target = TargetFactory.forTab(aTab || gBrowser.selectedTab); + gDevTools.showToolbox(target, "webconsole").then(toolbox => { + let hud = toolbox.getCurrentPanel().hud; + hud.jsterm._lazyVariablesView = false; + webconsoleOpened.resolve(hud); + }); + return webconsoleOpened.promise; +}; + +/** + * Close the Web Console for the given tab. + * + * @param nsIDOMElement [aTab] + * Optional tab element for which you want close the Web Console. The + * default tab is taken from the global variable |tab|. + * @param function [aCallback] + * Optional function to invoke after the Web Console completes + * closing (web-console-destroyed). + * @return object + * A promise that is resolved once the web console is closed. + */ +let closeConsole = Task.async(function* (aTab) { + let target = TargetFactory.forTab(aTab || gBrowser.selectedTab); + let toolbox = gDevTools.getToolbox(target); + if (toolbox) { + yield toolbox.destroy(); + } +}); + +/** + * Wait for a context menu popup to open. + * + * @param nsIDOMElement aPopup + * The XUL popup you expect to open. + * @param nsIDOMElement aButton + * The button/element that receives the contextmenu event. This is + * expected to open the popup. + * @param function aOnShown + * Function to invoke on popupshown event. + * @param function aOnHidden + * Function to invoke on popuphidden event. + * @return object + * A Promise object that is resolved after the popuphidden event + * callback is invoked. + */ +function waitForContextMenu(aPopup, aButton, aOnShown, aOnHidden) +{ + let deferred = promise.defer(); + + function onPopupShown() { + info("onPopupShown"); + aPopup.removeEventListener("popupshown", onPopupShown); + + aOnShown && aOnShown(); + + // Use executeSoon() to get out of the popupshown event. + aPopup.addEventListener("popuphidden", onPopupHidden); + executeSoon(() => aPopup.hidePopup()); + } + function onPopupHidden() { + info("onPopupHidden"); + aPopup.removeEventListener("popuphidden", onPopupHidden); + + aOnHidden && aOnHidden(); + + deferred.resolve(aPopup); + } + + aPopup.addEventListener("popupshown", onPopupShown); + + info("wait for the context menu to open"); + let eventDetails = {type: "contextmenu", button: 2}; + EventUtils.synthesizeMouse(aButton, 2, 2, eventDetails, + aButton.ownerDocument.defaultView); + return deferred.promise; +} + +/** + * Listen for a new tab to open and return a promise that resolves when one + * does and completes the load event. + * @return a promise that resolves to the tab object + */ +let waitForTab = Task.async(function*() { + info("Waiting for a tab to open"); + yield once(gBrowser.tabContainer, "TabOpen"); + let tab = gBrowser.selectedTab; + let browser = tab.linkedBrowser; + yield once(browser, "load", true); + info("The tab load completed"); + return tab; +}); + +/** + * Dump the output of all open Web Consoles - used only for debugging purposes. + */ +function dumpConsoles() +{ + if (gPendingOutputTest) { + console.log("dumpConsoles start"); + for (let [, hud] of HUDService.consoles) { + if (!hud.outputNode) { + console.debug("no output content for", hud.hudId); + continue; + } + + console.debug("output content for", hud.hudId); + for (let elem of hud.outputNode.childNodes) { + dumpMessageElement(elem); + } + } + console.log("dumpConsoles end"); + + gPendingOutputTest = 0; + } +} + +/** + * Dump to output debug information for the given webconsole message. + * + * @param nsIDOMNode aMessage + * The message element you want to display. + */ +function dumpMessageElement(aMessage) +{ + let text = aMessage.textContent; + let repeats = aMessage.querySelector(".message-repeats"); + if (repeats) { + repeats = repeats.getAttribute("value"); + } + console.debug("id", aMessage.getAttribute("id"), + "date", aMessage.timestamp, + "class", aMessage.className, + "category", aMessage.category, + "severity", aMessage.severity, + "repeats", repeats, + "clipboardText", aMessage.clipboardText, + "text", text); +} + +let finishTest = Task.async(function* () { + dumpConsoles(); + + let browserConsole = HUDService.getBrowserConsole(); + if (browserConsole) { + if (browserConsole.jsterm) { + browserConsole.jsterm.clearOutput(true); + } + yield HUDService.toggleBrowserConsole(); + } + + let target = TargetFactory.forTab(gBrowser.selectedTab); + yield gDevTools.closeToolbox(target); + + finish(); +}); + +registerCleanupFunction(function*() { + gDevTools.testing = false; + + // Remove stored console commands in between tests + yield asyncStorage.removeItem("webConsoleHistory"); + + dumpConsoles(); + + if (HUDService.getBrowserConsole()) { + HUDService.toggleBrowserConsole(); + } + + let target = TargetFactory.forTab(gBrowser.selectedTab); + yield gDevTools.closeToolbox(target); + + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } +}); + +waitForExplicitFinish(); + +/** + * Polls a given function waiting for it to become true. + * + * @param object aOptions + * Options object with the following properties: + * - validator + * A validator function that returns a boolean. This is called every few + * milliseconds to check if the result is true. When it is true, the + * promise is resolved and polling stops. If validator never returns + * true, then polling timeouts after several tries and the promise is + * rejected. + * - name + * Name of test. This is used to generate the success and failure + * messages. + * - timeout + * Timeout for validator function, in milliseconds. Default is 5000. + * @return object + * A Promise object that is resolved based on the validator function. + */ +function waitForSuccess(aOptions) +{ + let deferred = promise.defer(); + let start = Date.now(); + let timeout = aOptions.timeout || 5000; + let {validator} = aOptions; + + + function wait() + { + if ((Date.now() - start) > timeout) { + // Log the failure. + ok(false, "Timed out while waiting for: " + aOptions.name); + deferred.reject(null); + return; + } + + if (validator(aOptions)) { + ok(true, aOptions.name); + deferred.resolve(null); + } + else { + setTimeout(wait, 100); + } + } + + setTimeout(wait, 100); + + return deferred.promise; +} + +let openInspector = Task.async(function* (aTab = gBrowser.selectedTab) { + let target = TargetFactory.forTab(aTab); + let toolbox = yield gDevTools.showToolbox(target, "inspector"); + return toolbox.getCurrentPanel(); +}); + +/** + * Find variables or properties in a VariablesView instance. + * + * @param object aView + * The VariablesView instance. + * @param array aRules + * The array of rules you want to match. Each rule is an object with: + * - name (string|regexp): property name to match. + * - value (string|regexp): property value to match. + * - isIterator (boolean): check if the property is an iterator. + * - isGetter (boolean): check if the property is a getter. + * - isGenerator (boolean): check if the property is a generator. + * - dontMatch (boolean): make sure the rule doesn't match any property. + * @param object aOptions + * Options for matching: + * - webconsole: the WebConsole instance we work with. + * @return object + * A promise object that is resolved when all the rules complete + * matching. The resolved callback is given an array of all the rules + * you wanted to check. Each rule has a new property: |matchedProp| + * which holds a reference to the Property object instance from the + * VariablesView. If the rule did not match, then |matchedProp| is + * undefined. + */ +function findVariableViewProperties(aView, aRules, aOptions) +{ + // Initialize the search. + function init() + { + // Separate out the rules that require expanding properties throughout the + // view. + let expandRules = []; + let rules = aRules.filter((aRule) => { + if (typeof aRule.name == "string" && aRule.name.indexOf(".") > -1) { + expandRules.push(aRule); + return false; + } + return true; + }); + + // Search through the view those rules that do not require any properties to + // be expanded. Build the array of matchers, outstanding promises to be + // resolved. + let outstanding = []; + finder(rules, aView, outstanding); + + // Process the rules that need to expand properties. + let lastStep = processExpandRules.bind(null, expandRules); + + // Return the results - a promise resolved to hold the updated aRules array. + let returnResults = onAllRulesMatched.bind(null, aRules); + + return promise.all(outstanding).then(lastStep).then(returnResults); + } + + function onMatch(aProp, aRule, aMatched) + { + if (aMatched && !aRule.matchedProp) { + aRule.matchedProp = aProp; + } + } + + function finder(aRules, aVar, aPromises) + { + for (let [id, prop] of aVar) { + for (let rule of aRules) { + let matcher = matchVariablesViewProperty(prop, rule, aOptions); + aPromises.push(matcher.then(onMatch.bind(null, prop, rule))); + } + } + } + + function processExpandRules(aRules) + { + let rule = aRules.shift(); + if (!rule) { + return promise.resolve(null); + } + + let deferred = promise.defer(); + let expandOptions = { + rootVariable: aView, + expandTo: rule.name, + webconsole: aOptions.webconsole, + }; + + variablesViewExpandTo(expandOptions).then(function onSuccess(aProp) { + let name = rule.name; + let lastName = name.split(".").pop(); + rule.name = lastName; + + let matched = matchVariablesViewProperty(aProp, rule, aOptions); + return matched.then(onMatch.bind(null, aProp, rule)).then(function() { + rule.name = name; + }); + }, function onFailure() { + return promise.resolve(null); + }).then(processExpandRules.bind(null, aRules)).then(function() { + deferred.resolve(null); + }); + + return deferred.promise; + } + + function onAllRulesMatched(aRules) + { + for (let rule of aRules) { + let matched = rule.matchedProp; + if (matched && !rule.dontMatch) { + ok(true, "rule " + rule.name + " matched for property " + matched.name); + } + else if (matched && rule.dontMatch) { + ok(false, "rule " + rule.name + " should not match property " + + matched.name); + } + else { + ok(rule.dontMatch, "rule " + rule.name + " did not match any property"); + } + } + return aRules; + } + + return init(); +} + +/** + * Check if a given Property object from the variables view matches the given + * rule. + * + * @param object aProp + * The variable's view Property instance. + * @param object aRule + * Rules for matching the property. See findVariableViewProperties() for + * details. + * @param object aOptions + * Options for matching. See findVariableViewProperties(). + * @return object + * A promise that is resolved when all the checks complete. Resolution + * result is a boolean that tells your promise callback the match + * result: true or false. + */ +function matchVariablesViewProperty(aProp, aRule, aOptions) +{ + function resolve(aResult) { + return promise.resolve(aResult); + } + + if (aRule.name) { + let match = aRule.name instanceof RegExp ? + aRule.name.test(aProp.name) : + aProp.name == aRule.name; + if (!match) { + return resolve(false); + } + } + + if (aRule.value) { + let displayValue = aProp.displayValue; + if (aProp.displayValueClassName == "token-string") { + displayValue = displayValue.substring(1, displayValue.length - 1); + } + + let match = aRule.value instanceof RegExp ? + aRule.value.test(displayValue) : + displayValue == aRule.value; + if (!match) { + info("rule " + aRule.name + " did not match value, expected '" + + aRule.value + "', found '" + displayValue + "'"); + return resolve(false); + } + } + + if ("isGetter" in aRule) { + let isGetter = !!(aProp.getter && aProp.get("get")); + if (aRule.isGetter != isGetter) { + info("rule " + aRule.name + " getter test failed"); + return resolve(false); + } + } + + if ("isGenerator" in aRule) { + let isGenerator = aProp.displayValue == "Generator"; + if (aRule.isGenerator != isGenerator) { + info("rule " + aRule.name + " generator test failed"); + return resolve(false); + } + } + + let outstanding = []; + + if ("isIterator" in aRule) { + let isIterator = isVariableViewPropertyIterator(aProp, aOptions.webconsole); + outstanding.push(isIterator.then((aResult) => { + if (aResult != aRule.isIterator) { + info("rule " + aRule.name + " iterator test failed"); + } + return aResult == aRule.isIterator; + })); + } + + outstanding.push(promise.resolve(true)); + + return promise.all(outstanding).then(function _onMatchDone(aResults) { + let ruleMatched = aResults.indexOf(false) == -1; + return resolve(ruleMatched); + }); +} + +/** + * Check if the given variables view property is an iterator. + * + * @param object aProp + * The Property instance you want to check. + * @param object aWebConsole + * The WebConsole instance to work with. + * @return object + * A promise that is resolved when the check completes. The resolved + * callback is given a boolean: true if the property is an iterator, or + * false otherwise. + */ +function isVariableViewPropertyIterator(aProp, aWebConsole) +{ + if (aProp.displayValue == "Iterator") { + return promise.resolve(true); + } + + let deferred = promise.defer(); + + variablesViewExpandTo({ + rootVariable: aProp, + expandTo: "__proto__.__iterator__", + webconsole: aWebConsole, + }).then(function onSuccess(aProp) { + deferred.resolve(true); + }, function onFailure() { + deferred.resolve(false); + }); + + return deferred.promise; +} + + +/** + * Recursively expand the variables view up to a given property. + * + * @param aOptions + * Options for view expansion: + * - rootVariable: start from the given scope/variable/property. + * - expandTo: string made up of property names you want to expand. + * For example: "body.firstChild.nextSibling" given |rootVariable: + * document|. + * - webconsole: a WebConsole instance. If this is not provided all + * property expand() calls will be considered sync. Things may fail! + * @return object + * A promise that is resolved only when the last property in |expandTo| + * is found, and rejected otherwise. Resolution reason is always the + * last property - |nextSibling| in the example above. Rejection is + * always the last property that was found. + */ +function variablesViewExpandTo(aOptions) +{ + let root = aOptions.rootVariable; + let expandTo = aOptions.expandTo.split("."); + let jsterm = (aOptions.webconsole || {}).jsterm; + let lastDeferred = promise.defer(); + + function fetch(aProp) + { + if (!aProp.onexpand) { + ok(false, "property " + aProp.name + " cannot be expanded: !onexpand"); + return promise.reject(aProp); + } + + let deferred = promise.defer(); + + if (aProp._fetched || !jsterm) { + executeSoon(function() { + deferred.resolve(aProp); + }); + } + else { + jsterm.once("variablesview-fetched", function _onFetchProp() { + executeSoon(() => deferred.resolve(aProp)); + }); + } + + aProp.expand(); + + return deferred.promise; + } + + function getNext(aProp) + { + let name = expandTo.shift(); + let newProp = aProp.get(name); + + if (expandTo.length > 0) { + ok(newProp, "found property " + name); + if (newProp) { + fetch(newProp).then(getNext, fetchError); + } + else { + lastDeferred.reject(aProp); + } + } + else { + if (newProp) { + lastDeferred.resolve(newProp); + } + else { + lastDeferred.reject(aProp); + } + } + } + + function fetchError(aProp) + { + lastDeferred.reject(aProp); + } + + if (!root._fetched) { + fetch(root).then(getNext, fetchError); + } + else { + getNext(root); + } + + return lastDeferred.promise; +} + + +/** + * Update the content of a property in the variables view. + * + * @param object aOptions + * Options for the property update: + * - property: the property you want to change. + * - field: string that tells what you want to change: + * - use "name" to change the property name, + * - or "value" to change the property value. + * - string: the new string to write into the field. + * - webconsole: reference to the Web Console instance we work with. + * @return object + * A Promise object that is resolved once the property is updated. + */ +let updateVariablesViewProperty = Task.async(function* (aOptions) { + let view = aOptions.property._variablesView; + view.window.focus(); + aOptions.property.focus(); + + switch (aOptions.field) { + case "name": + EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, view.window); + break; + case "value": + EventUtils.synthesizeKey("VK_RETURN", {}, view.window); + break; + default: + throw new Error("options.field is incorrect"); + } + + let deferred = promise.defer(); + + executeSoon(() => { + EventUtils.synthesizeKey("A", { accelKey: true }, view.window); + + for (let c of aOptions.string) { + EventUtils.synthesizeKey(c, {}, view.window); + } + + if (aOptions.webconsole) { + aOptions.webconsole.jsterm.once("variablesview-fetched").then((varView) => { + deferred.resolve(varView); + }); + } + + EventUtils.synthesizeKey("VK_RETURN", {}, view.window); + + if (!aOptions.webconsole) { + executeSoon(() => { + deferred.resolve(null); + }); + } + }); + + return deferred.promise; +}); + +/** + * Open the JavaScript debugger. + * + * @param object aOptions + * Options for opening the debugger: + * - tab: the tab you want to open the debugger for. + * @return object + * A promise that is resolved once the debugger opens, or rejected if + * the open fails. The resolution callback is given one argument, an + * object that holds the following properties: + * - target: the Target object for the Tab. + * - toolbox: the Toolbox instance. + * - panel: the jsdebugger panel instance. + * - panelWin: the window object of the panel iframe. + */ +function openDebugger(aOptions = {}) +{ + if (!aOptions.tab) { + aOptions.tab = gBrowser.selectedTab; + } + + let deferred = promise.defer(); + + let target = TargetFactory.forTab(aOptions.tab); + let toolbox = gDevTools.getToolbox(target); + let dbgPanelAlreadyOpen = toolbox && toolbox.getPanel("jsdebugger"); + + gDevTools.showToolbox(target, "jsdebugger").then(function onSuccess(aToolbox) { + let panel = aToolbox.getCurrentPanel(); + let panelWin = panel.panelWin; + + panel._view.Variables.lazyEmpty = false; + + let resolveObject = { + target: target, + toolbox: aToolbox, + panel: panel, + panelWin: panelWin, + }; + + if (dbgPanelAlreadyOpen) { + deferred.resolve(resolveObject); + } + else { + panelWin.once(panelWin.EVENTS.SOURCES_ADDED, () => { + deferred.resolve(resolveObject); + }); + } + }, function onFailure(aReason) { + console.debug("failed to open the toolbox for 'jsdebugger'", aReason); + deferred.reject(aReason); + }); + + return deferred.promise; +} + +/** + * Returns true if the caret in the debugger editor is placed at the specified + * position. + * @param aPanel The debugger panel. + * @param {number} aLine The line number. + * @param {number} [aCol] The column number. + * @returns {boolean} + */ +function isDebuggerCaretPos(aPanel, aLine, aCol = 1) { + let editor = aPanel.panelWin.DebuggerView.editor; + let cursor = editor.getCursor(); + + // Source editor starts counting line and column numbers from 0. + info("Current editor caret position: " + (cursor.line + 1) + ", " + + (cursor.ch + 1)); + return cursor.line == (aLine - 1) && cursor.ch == (aCol - 1); +} + +/** + * Wait for messages in the Web Console output. + * + * @param object aOptions + * Options for what you want to wait for: + * - webconsole: the webconsole instance you work with. + * - matchCondition: "any" or "all". Default: "all". The promise + * returned by this function resolves when all of the messages are + * matched, if the |matchCondition| is "all". If you set the condition to + * "any" then the promise is resolved by any message rule that matches, + * irrespective of order - waiting for messages stops whenever any rule + * matches. + * - messages: an array of objects that tells which messages to wait for. + * Properties: + * - text: string or RegExp to match the textContent of each new + * message. + * - noText: string or RegExp that must not match in the message + * textContent. + * - repeats: the number of message repeats, as displayed by the Web + * Console. + * - category: match message category. See CATEGORY_* constants at + * the top of this file. + * - severity: match message severity. See SEVERITY_* constants at + * the top of this file. + * - count: how many unique web console messages should be matched by + * this rule. + * - consoleTrace: boolean, set to |true| to match a console.trace() + * message. Optionally this can be an object of the form + * { file, fn, line } that can match the specified file, function + * and/or line number in the trace message. + * - consoleTime: string that matches a console.time() timer name. + * Provide this if you want to match a console.time() message. + * - consoleTimeEnd: same as above, but for console.timeEnd(). + * - consoleDir: boolean, set to |true| to match a console.dir() + * message. + * - consoleGroup: boolean, set to |true| to match a console.group() + * message. + * - consoleTable: boolean, set to |true| to match a console.table() + * message. + * - longString: boolean, set to |true} to match long strings in the + * message. + * - collapsible: boolean, set to |true| to match messages that can + * be collapsed/expanded. + * - type: match messages that are instances of the given object. For + * example, you can point to Messages.NavigationMarker to match any + * such message. + * - objects: boolean, set to |true| if you expect inspectable + * objects in the message. + * - source: object of the shape { url, line }. This is used to + * match the source URL and line number of the error message or + * console API call. + * - prefix: prefix text to check for in the prefix element. + * - stacktrace: array of objects of the form { file, fn, line } that + * can match frames in the stacktrace associated with the message. + * - groupDepth: number used to check the depth of the message in + * a group. + * - url: URL to match for network requests. + * @return object + * A promise object is returned once the messages you want are found. + * The promise is resolved with the array of rule objects you give in + * the |messages| property. Each objects is the same as provided, with + * additional properties: + * - matched: a Set of web console messages that matched the rule. + * - clickableElements: a list of inspectable objects. This is available + * if any of the following properties are present in the rule: + * |consoleTrace| or |objects|. + * - longStrings: a list of long string ellipsis elements you can click + * in the message element, to expand a long string. This is available + * only if |longString| is present in the matching rule. + */ +function waitForMessages(aOptions) +{ + info("Waiting for messages..."); + + gPendingOutputTest++; + let webconsole = aOptions.webconsole; + let rules = WebConsoleUtils.cloneObject(aOptions.messages, true); + let rulesMatched = 0; + let listenerAdded = false; + let deferred = promise.defer(); + aOptions.matchCondition = aOptions.matchCondition || "all"; + + function checkText(aRule, aText) + { + let result = false; + if (Array.isArray(aRule)) { + result = aRule.every((s) => checkText(s, aText)); + } + else if (typeof aRule == "string") { + result = aText.indexOf(aRule) > -1; + } + else if (aRule instanceof RegExp) { + result = aRule.test(aText); + } + else { + result = aRule == aText; + } + return result; + } + + function checkConsoleTable(aRule, aElement) + { + let elemText = aElement.textContent; + let table = aRule.consoleTable; + + if (!checkText("console.table():", elemText)) { + return false; + } + + aRule.category = CATEGORY_WEBDEV; + aRule.severity = SEVERITY_LOG; + aRule.type = Messages.ConsoleTable; + + return true; + } + + function checkConsoleTrace(aRule, aElement) + { + let elemText = aElement.textContent; + let trace = aRule.consoleTrace; + + if (!checkText("console.trace():", elemText)) { + return false; + } + + aRule.category = CATEGORY_WEBDEV; + aRule.severity = SEVERITY_LOG; + aRule.type = Messages.ConsoleTrace; + + if (!aRule.stacktrace && typeof trace == "object" && trace !== true) { + if (Array.isArray(trace)) { + aRule.stacktrace = trace; + } else { + aRule.stacktrace = [trace]; + } + } + + return true; + } + + function checkConsoleTime(aRule, aElement) + { + let elemText = aElement.textContent; + let time = aRule.consoleTime; + + if (!checkText(time + ": timer started", elemText)) { + return false; + } + + aRule.category = CATEGORY_WEBDEV; + aRule.severity = SEVERITY_LOG; + + return true; + } + + function checkConsoleTimeEnd(aRule, aElement) + { + let elemText = aElement.textContent; + let time = aRule.consoleTimeEnd; + let regex = new RegExp(time + ": -?\\d+([,.]\\d+)?ms"); + + if (!checkText(regex, elemText)) { + return false; + } + + aRule.category = CATEGORY_WEBDEV; + aRule.severity = SEVERITY_LOG; + + return true; + } + + function checkConsoleDir(aRule, aElement) + { + if (!aElement.classList.contains("inlined-variables-view")) { + return false; + } + + let elemText = aElement.textContent; + if (!checkText(aRule.consoleDir, elemText)) { + return false; + } + + let iframe = aElement.querySelector("iframe"); + if (!iframe) { + ok(false, "console.dir message has no iframe"); + return false; + } + + return true; + } + + function checkConsoleGroup(aRule, aElement) + { + if (!isNaN(parseInt(aRule.consoleGroup))) { + aRule.groupDepth = aRule.consoleGroup; + } + aRule.category = CATEGORY_WEBDEV; + aRule.severity = SEVERITY_LOG; + + return true; + } + + function checkSource(aRule, aElement) + { + let location = aElement.querySelector(".message-location"); + if (!location) { + return false; + } + + if (!checkText(aRule.source.url, location.getAttribute("title"))) { + return false; + } + + if ("line" in aRule.source && location.sourceLine != aRule.source.line) { + return false; + } + + return true; + } + + function checkCollapsible(aRule, aElement) + { + let msg = aElement._messageObject; + if (!msg || !!msg.collapsible != aRule.collapsible) { + return false; + } + + return true; + } + + function checkStacktrace(aRule, aElement) + { + let stack = aRule.stacktrace; + let frames = aElement.querySelectorAll(".stacktrace > li"); + if (!frames.length) { + return false; + } + + for (let i = 0; i < stack.length; i++) { + let frame = frames[i]; + let expected = stack[i]; + if (!frame) { + ok(false, "expected frame #" + i + " but didnt find it"); + return false; + } + + if (expected.file) { + let file = frame.querySelector(".message-location").title; + if (!checkText(expected.file, file)) { + ok(false, "frame #" + i + " does not match file name: " + + expected.file); + displayErrorContext(aRule, aElement); + return false; + } + } + + if (expected.fn) { + let fn = frame.querySelector(".function").textContent; + if (!checkText(expected.fn, fn)) { + ok(false, "frame #" + i + " does not match the function name: " + + expected.fn); + displayErrorContext(aRule, aElement); + return false; + } + } + + if (expected.line) { + let line = frame.querySelector(".message-location").sourceLine; + if (!checkText(expected.line, line)) { + ok(false, "frame #" + i + " does not match the line number: " + + expected.line); + displayErrorContext(aRule, aElement); + return false; + } + } + } + + return true; + } + + function hasXhrLabel(aElement) { + let xhr = aElement.querySelector('.xhr'); + if (!xhr) { + return false; + } + return true; + } + + function checkMessage(aRule, aElement) + { + let elemText = aElement.textContent; + + if (aRule.text && !checkText(aRule.text, elemText)) { + return false; + } + + if (aRule.noText && checkText(aRule.noText, elemText)) { + return false; + } + + if (aRule.consoleTable && !checkConsoleTable(aRule, aElement)) { + return false; + } + + if (aRule.consoleTrace && !checkConsoleTrace(aRule, aElement)) { + return false; + } + + if (aRule.consoleTime && !checkConsoleTime(aRule, aElement)) { + return false; + } + + if (aRule.consoleTimeEnd && !checkConsoleTimeEnd(aRule, aElement)) { + return false; + } + + if (aRule.consoleDir && !checkConsoleDir(aRule, aElement)) { + return false; + } + + if (aRule.consoleGroup && !checkConsoleGroup(aRule, aElement)) { + return false; + } + + if (aRule.source && !checkSource(aRule, aElement)) { + return false; + } + + if ("collapsible" in aRule && !checkCollapsible(aRule, aElement)) { + return false; + } + + if (aRule.isXhr && !hasXhrLabel(aElement)) { + return false; + } + + if (!aRule.isXhr && hasXhrLabel(aElement)) { + return false; + } + + let partialMatch = !!(aRule.consoleTrace || aRule.consoleTime || + aRule.consoleTimeEnd); + + // The rule tries to match the newer types of messages, based on their + // object constructor. + if (aRule.type) { + if (!aElement._messageObject || + !(aElement._messageObject instanceof aRule.type)) { + if (partialMatch) { + ok(false, "message type for rule: " + displayRule(aRule)); + displayErrorContext(aRule, aElement); + } + return false; + } + partialMatch = true; + } + + if ("category" in aRule && aElement.category != aRule.category) { + if (partialMatch) { + is(aElement.category, aRule.category, + "message category for rule: " + displayRule(aRule)); + displayErrorContext(aRule, aElement); + } + return false; + } + + if ("severity" in aRule && aElement.severity != aRule.severity) { + if (partialMatch) { + is(aElement.severity, aRule.severity, + "message severity for rule: " + displayRule(aRule)); + displayErrorContext(aRule, aElement); + } + return false; + } + + if (aRule.text) { + partialMatch = true; + } + + if (aRule.stacktrace && !checkStacktrace(aRule, aElement)) { + if (partialMatch) { + ok(false, "failed to match stacktrace for rule: " + displayRule(aRule)); + displayErrorContext(aRule, aElement); + } + return false; + } + + if (aRule.category == CATEGORY_NETWORK && "url" in aRule && + !checkText(aRule.url, aElement.url)) { + return false; + } + + if ("repeats" in aRule) { + let repeats = aElement.querySelector(".message-repeats"); + if (!repeats || repeats.getAttribute("value") != aRule.repeats) { + return false; + } + } + + if ("groupDepth" in aRule) { + let indentNode = aElement.querySelector(".indent"); + let indent = (GROUP_INDENT * aRule.groupDepth) + "px"; + if (!indentNode || indentNode.style.width != indent) { + is(indentNode.style.width, indent, + "group depth check failed for message rule: " + displayRule(aRule)); + return false; + } + } + + if ("longString" in aRule) { + let longStrings = aElement.querySelectorAll(".longStringEllipsis"); + if (aRule.longString != !!longStrings[0]) { + if (partialMatch) { + is(!!longStrings[0], aRule.longString, + "long string existence check failed for message rule: " + + displayRule(aRule)); + displayErrorContext(aRule, aElement); + } + return false; + } + aRule.longStrings = longStrings; + } + + if ("objects" in aRule) { + let clickables = aElement.querySelectorAll(".message-body a"); + if (aRule.objects != !!clickables[0]) { + if (partialMatch) { + is(!!clickables[0], aRule.objects, + "objects existence check failed for message rule: " + + displayRule(aRule)); + displayErrorContext(aRule, aElement); + } + return false; + } + aRule.clickableElements = clickables; + } + + if ("prefix" in aRule) { + let prefixNode = aElement.querySelector(".prefix"); + is(prefixNode && prefixNode.textContent, aRule.prefix, "Check prefix"); + } + + let count = aRule.count || 1; + if (!aRule.matched) { + aRule.matched = new Set(); + } + aRule.matched.add(aElement); + + return aRule.matched.size == count; + } + + function onMessagesAdded(aEvent, aNewMessages) + { + for (let msg of aNewMessages) { + let elem = msg.node; + let location = elem.querySelector(".message-location"); + if (location) { + let url = location.title; + // Prevent recursion with the browser console and any potential + // messages coming from head.js. + if (url.indexOf("browser/devtools/webconsole/test/head.js") != -1) { + continue; + } + } + + for (let rule of rules) { + if (rule._ruleMatched) { + continue; + } + + let matched = checkMessage(rule, elem); + if (matched) { + rule._ruleMatched = true; + rulesMatched++; + ok(1, "matched rule: " + displayRule(rule)); + if (maybeDone()) { + return; + } + } + } + } + } + + function allRulesMatched() + { + return aOptions.matchCondition == "all" && rulesMatched == rules.length || + aOptions.matchCondition == "any" && rulesMatched > 0; + } + + function maybeDone() + { + if (allRulesMatched()) { + if (listenerAdded) { + webconsole.ui.off("new-messages", onMessagesAdded); + } + gPendingOutputTest--; + deferred.resolve(rules); + return true; + } + return false; + } + + function testCleanup() { + if (allRulesMatched()) { + return; + } + + if (webconsole.ui) { + webconsole.ui.off("new-messages", onMessagesAdded); + } + + for (let rule of rules) { + if (!rule._ruleMatched) { + ok(false, "failed to match rule: " + displayRule(rule)); + } + } + } + + function displayRule(aRule) + { + return aRule.name || aRule.text; + } + + function displayErrorContext(aRule, aElement) + { + console.log("error occured during rule " + displayRule(aRule)); + console.log("while checking the following message"); + dumpMessageElement(aElement); + } + + executeSoon(() => { + + let messages = []; + for (let elem of webconsole.outputNode.childNodes) { + messages.push({ + node: elem, + update: false, + }); + } + + onMessagesAdded("new-messages", messages); + + if (!allRulesMatched()) { + listenerAdded = true; + registerCleanupFunction(testCleanup); + webconsole.ui.on("new-messages", onMessagesAdded); + } + }); + + return deferred.promise; +} + +function whenDelayedStartupFinished(aWindow, aCallback) +{ + Services.obs.addObserver(function observer(aSubject, aTopic) { + if (aWindow == aSubject) { + Services.obs.removeObserver(observer, aTopic); + executeSoon(aCallback); + } + }, "browser-delayed-startup-finished", false); +} + +/** + * Check the web console output for the given inputs. Each input is checked for + * the expected JS eval result, the result of calling print(), the result of + * console.log(). The JS eval result is also checked if it opens the variables + * view on click. + * + * @param object hud + * The web console instance to work with. + * @param array inputTests + * An array of input tests. An input test element is an object. Each + * object has the following properties: + * - input: string, JS input value to execute. + * + * - output: string|RegExp, expected JS eval result. + * + * - inspectable: boolean, when true, the test runner expects the JS eval + * result is an object that can be clicked for inspection. + * + * - noClick: boolean, when true, the test runner does not click the JS + * eval result. Some objects, like |window|, have a lot of properties and + * opening vview for them is very slow (they can cause timeouts in debug + * builds). + * + * - consoleOutput: string|RegExp, optional, expected consoleOutput + * If not provided consoleOuput = output; + * + * - printOutput: string|RegExp, optional, expected output for + * |print(input)|. If this is not provided, printOutput = output. + * + * - variablesViewLabel: string|RegExp, optional, the expected variables + * view label when the object is inspected. If this is not provided, then + * |output| is used. + * + * - inspectorIcon: boolean, when true, the test runner expects the + * result widget to contain an inspectorIcon element (className + * open-inspector). + * + * - expectedTab: string, optional, the full URL of the new tab which must + * open. If this is not provided, any new tabs that open will cause a test + * failure. + */ +function checkOutputForInputs(hud, inputTests) +{ + let container = gBrowser.tabContainer; + + function* runner() + { + for (let [i, entry] of inputTests.entries()) { + info("checkInput(" + i + "): " + entry.input); + yield checkInput(entry); + } + container = null; + } + + function* checkInput(entry) + { + yield checkConsoleLog(entry); + yield checkPrintOutput(entry); + yield checkJSEval(entry); + } + + function* checkConsoleLog(entry) + { + info("Logging: " + entry.input); + hud.jsterm.clearOutput(); + hud.jsterm.execute("console.log(" + entry.input + ")"); + + let consoleOutput = "consoleOutput" in entry ? + entry.consoleOutput : entry.output; + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "console.log() output: " + consoleOutput, + text: consoleOutput, + category: CATEGORY_WEBDEV, + severity: SEVERITY_LOG, + }], + }); + + if (typeof entry.inspectorIcon == "boolean") { + let msg = [...result.matched][0]; + info("Checking Inspector Link: " + entry.input); + yield checkLinkToInspector(entry.inspectorIcon, msg); + } + } + + function checkPrintOutput(entry) + { + info("Printing: " + entry.input); + hud.jsterm.clearOutput(); + hud.jsterm.execute("print(" + entry.input + ")"); + + let printOutput = entry.printOutput || entry.output; + + return waitForMessages({ + webconsole: hud, + messages: [{ + name: "print() output: " + printOutput, + text: printOutput, + category: CATEGORY_OUTPUT, + }], + }); + } + + function* checkJSEval(entry) + { + info("Evaluating: " + entry.input); + hud.jsterm.clearOutput(); + hud.jsterm.execute(entry.input); + + let [result] = yield waitForMessages({ + webconsole: hud, + messages: [{ + name: "JS eval output: " + entry.output, + text: entry.output, + category: CATEGORY_OUTPUT, + }], + }); + + let msg = [...result.matched][0]; + if (!entry.noClick) { + yield checkObjectClick(entry, msg); + } + if (typeof entry.inspectorIcon == "boolean") { + info("Checking Inspector Link: " + entry.input); + yield checkLinkToInspector(entry.inspectorIcon, msg); + } + } + + function* checkObjectClick(entry, msg) + { + info("Clicking: " + entry.input); + let body = msg.querySelector(".message-body a") || + msg.querySelector(".message-body"); + ok(body, "the message body"); + + let deferredVariablesView = promise.defer(); + entry._onVariablesViewOpen = onVariablesViewOpen.bind(null, entry, deferredVariablesView); + hud.jsterm.on("variablesview-open", entry._onVariablesViewOpen); + + let deferredTab = promise.defer(); + entry._onTabOpen = onTabOpen.bind(null, entry, deferredTab); + container.addEventListener("TabOpen", entry._onTabOpen, true); + + body.scrollIntoView(); + EventUtils.synthesizeMouse(body, 2, 2, {}, hud.iframeWindow); + + if (entry.inspectable) { + info("message body tagName '" + body.tagName + "' className '" + body.className + "'"); + yield deferredVariablesView.promise; + } else { + hud.jsterm.off("variablesview-open", entry._onVariablesView); + entry._onVariablesView = null; + } + + if (entry.expectedTab) { + yield deferredTab.promise; + } else { + container.removeEventListener("TabOpen", entry._onTabOpen, true); + entry._onTabOpen = null; + } + + yield promise.resolve(null); + } + + function onVariablesViewOpen(entry, {resolve, reject}, event, view, options) + { + info("Variables view opened: " + entry.input); + let label = entry.variablesViewLabel || entry.output; + if (typeof label == "string" && options.label != label) { + return; + } + if (label instanceof RegExp && !label.test(options.label)) { + return; + } + + hud.jsterm.off("variablesview-open", entry._onVariablesViewOpen); + entry._onVariablesViewOpen = null; + ok(entry.inspectable, "variables view was shown"); + + resolve(null); + } + + function onTabOpen(entry, {resolve, reject}, event) + { + container.removeEventListener("TabOpen", entry._onTabOpen, true); + entry._onTabOpen = null; + + let tab = event.target; + let browser = gBrowser.getBrowserForTab(tab); + loadBrowser(browser).then(() => { + let uri = content.location.href; + ok(entry.expectedTab && entry.expectedTab == uri, + "opened tab '" + uri + "', expected tab '" + entry.expectedTab + "'"); + return closeTab(tab); + }).then(resolve, reject); + } + + return Task.spawn(runner); +} + +/** + * Wait for eventName on target. + * @param {Object} target An observable object that either supports on/off or + * addEventListener/removeEventListener + * @param {String} eventName + * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener + * @return A promise that resolves when the event has been handled + */ +function once(target, eventName, useCapture=false) { + info("Waiting for event: '" + eventName + "' on " + target + "."); + + let deferred = promise.defer(); + + for (let [add, remove] of [ + ["addEventListener", "removeEventListener"], + ["addListener", "removeListener"], + ["on", "off"] + ]) { + if ((add in target) && (remove in target)) { + target[add](eventName, function onEvent(...aArgs) { + target[remove](eventName, onEvent, useCapture); + deferred.resolve.apply(deferred, aArgs); + }, useCapture); + break; + } + } + + return deferred.promise; +} + +/** + * Checks a link to the inspector + * + * @param {boolean} hasLinkToInspector Set to true if the message should + * link to the inspector panel. + * @param {element} msg The message to test. + */ +function checkLinkToInspector(hasLinkToInspector, msg) +{ + let elementNodeWidget = [...msg._messageObject.widgets][0]; + if (!elementNodeWidget) { + ok(!hasLinkToInspector, "The message has no ElementNode widget"); + return; + } + + return elementNodeWidget.linkToInspector().then(() => { + // linkToInspector resolved, check for the .open-inspector element + if (hasLinkToInspector) { + ok(msg.querySelectorAll(".open-inspector").length, + "The ElementNode widget is linked to the inspector"); + } else { + ok(!msg.querySelectorAll(".open-inspector").length, + "The ElementNode widget isn't linked to the inspector"); + } + }, () => { + // linkToInspector promise rejected, node not linked to inspector + ok(!hasLinkToInspector, "The ElementNode widget isn't linked to the inspector"); + }); +} + +function getSourceActor(aSources, aURL) { + let item = aSources.getItemForAttachment(a => a.source.url === aURL); + return item && item.value; +} + +/** + * Verify that clicking on a link from a popup notification message tries to + * open the expected URL. + */ +function simulateMessageLinkClick(element, expectedLink) { + let deferred = promise.defer(); + + // Invoke the click event and check if a new tab would + // open to the correct page. + let oldOpenUILinkIn = window.openUILinkIn; + window.openUILinkIn = function(link) { + if (link == expectedLink) { + ok(true, "Clicking the message link opens the desired page"); + window.openUILinkIn = oldOpenUILinkIn; + deferred.resolve(); + } + }; + + let event = new MouseEvent("click", { + detail: 1, + button: 0, + bubbles: true, + cancelable: true + }); + element.dispatchEvent(event); + + return deferred.promise; +} diff --git a/toolkit/devtools/webconsole/test/test-bug_1050691_click_function_to_source.html b/toolkit/devtools/webconsole/test/test-bug_1050691_click_function_to_source.html new file mode 100644 index 0000000000..912e301f02 --- /dev/null +++ b/toolkit/devtools/webconsole/test/test-bug_1050691_click_function_to_source.html @@ -0,0 +1,11 @@ + + + + + Click on function should point to source + + + + + diff --git a/toolkit/devtools/webconsole/test/test-bug_1050691_click_function_to_source.js b/toolkit/devtools/webconsole/test/test-bug_1050691_click_function_to_source.js new file mode 100644 index 0000000000..1eddf0d6ed --- /dev/null +++ b/toolkit/devtools/webconsole/test/test-bug_1050691_click_function_to_source.js @@ -0,0 +1,10 @@ +/** + * this + * is + * a + * function + */ +function foo() { + console.log(foo); +} + diff --git a/toolkit/devtools/webconsole/test/test_jsterm_last_result.html b/toolkit/devtools/webconsole/test/test_jsterm_last_result.html new file mode 100644 index 0000000000..277abdbc7d --- /dev/null +++ b/toolkit/devtools/webconsole/test/test_jsterm_last_result.html @@ -0,0 +1,130 @@ + + + + + Test for the $_ getter + + + + + +

Test for the $_ getter

+ + + + + + diff --git a/toolkit/devtools/webconsole/utils.js b/toolkit/devtools/webconsole/utils.js index d228c5e07c..f3b23a2901 100644 --- a/toolkit/devtools/webconsole/utils.js +++ b/toolkit/devtools/webconsole/utils.js @@ -1552,6 +1552,20 @@ function JSTermHelpers(aOwner) return aOwner.window.document.querySelectorAll(aSelector); }; + /** + * Returns the result of the last console input evaluation + * + * @return object|undefined + * Returns last console evaluation or undefined + */ + Object.defineProperty(aOwner.sandbox, "$_", { + get: function() { + return aOwner.consoleActor.getLastConsoleInputEvaluation(); + }, + enumerable: true, + configurable: true + }); + /** * Runs an xPath query and returns all matched nodes. * diff --git a/toolkit/devtools/webconsole/webconsole.xul b/toolkit/devtools/webconsole/webconsole.xul index 8604549654..5db23074ef 100644 --- a/toolkit/devtools/webconsole/webconsole.xul +++ b/toolkit/devtools/webconsole/webconsole.xul @@ -76,6 +76,8 @@ function goUpdateConsoleCommands() { + diff --git a/toolkit/locales/en-US/chrome/global/devtools/debugger.properties b/toolkit/locales/en-US/chrome/global/devtools/debugger.properties index 10f5cf82ae..7b1ff9c36e 100644 --- a/toolkit/locales/en-US/chrome/global/devtools/debugger.properties +++ b/toolkit/locales/en-US/chrome/global/devtools/debugger.properties @@ -214,9 +214,9 @@ breakpointMenuItem.deleteAll=Remove all breakpoints # yet. loadingText=Loading\u2026 -# LOCALIZATION NOTE (errorLoadingText): The text that is displayed in the debugger +# LOCALIZATION NOTE (errorLoadingText2): The text that is displayed in the debugger # viewer when there is an error loading a file -errorLoadingText=Error loading source:\n +errorLoadingText2=Error loading this URL: %S # LOCALIZATION NOTE (addWatchExpressionText): The text that is displayed in the # watch expressions list to add a new item. @@ -320,4 +320,4 @@ variablesViewOptimizedOut=(optimized away) variablesViewUninitialized=(uninitialized) variablesViewMissingArgs=(unavailable) -evalGroupLabel=Evaluated Sources \ No newline at end of file +evalGroupLabel=Evaluated Sources diff --git a/toolkit/locales/en-US/chrome/global/devtools/webConsole.dtd b/toolkit/locales/en-US/chrome/global/devtools/webConsole.dtd index 9bb5394d23..f7a3a7a71a 100644 --- a/toolkit/locales/en-US/chrome/global/devtools/webConsole.dtd +++ b/toolkit/locales/en-US/chrome/global/devtools/webConsole.dtd @@ -97,3 +97,5 @@ + + diff --git a/toolkit/modules/Promise-backend.js b/toolkit/modules/Promise-backend.js index 8d2e4fd08d..bf2105384f 100644 --- a/toolkit/modules/Promise-backend.js +++ b/toolkit/modules/Promise-backend.js @@ -22,9 +22,30 @@ //////////////////////////////////////////////////////////////////////////////// //// Globals -// Do not load the FinalizationWitnessService is we are being required as a -// CommonJS module, because the Components object is not available in workers. -if (!isWorker) { +// Obtain an instance of Cu. How this instance is obtained depends on how this +// file is loaded. +// +// This file can be loaded in three different ways: +// 1. As a CommonJS module, by Loader.jsm, on the main thread. +// 2. As a CommonJS module, by worker-loader.js, on a worker thread. +// 3. As a subscript, by Promise.jsm, on the main thread. +// +// If require is defined, the file is loaded as a CommonJS module. Components +// will not be defined in that case, but we can obtain an instance of Cu from +// the chrome module. Otherwise, this file is loaded as a subscript, and we can +// obtain an instance of Cu from Components directly. +// +// If the file is loaded as a CommonJS module on a worker thread, the instance +// of Cu obtained from the chrome module will be null. The reason for this is +// that Components is not defined in worker threads, so no instance of Cu can +// be obtained. + +let Cu = this.require ? require("chrome").Cu : Components.utils; +let Cc = this.require ? require("chrome").Cc : Components.classes; +let Ci = this.require ? require("chrome").Ci : Components.interfaces; + +// If Cu is defined, use it to lazily define the FinalizationWitnessService. +if (Cu) { Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -46,7 +67,7 @@ const salt = Math.floor(Math.random() * 100); const N_INTERNALS = "{private:internals:" + salt + "}"; // We use DOM Promise for scheduling the walker loop. -const DOMPromise = isWorker ? null : Promise; +const DOMPromise = Cu ? Promise : null; /////// Warn-upon-finalization mechanism // @@ -252,10 +273,9 @@ let PendingErrors = { } }; -// Do not initialize the warn-on-finalization mechanism if we are being required -// as a CommonJS module by the worker loader, because the Components object (and -// therefore the FinalizationWitnessService) is not available. -if (!isWorker) { +// Initialize the warn-upon-finalization mechanism if and only if Cu is defined. +// Otherwise, FinalizationWitnessService won't be defined (see above). +if (Cu) { PendingErrors.init(); } @@ -628,9 +648,9 @@ Object.freeze(Promise.Debugging); Object.freeze(Promise); -// Make sure to export the Promise object if we are being required as a CommonJS -// module by the worker loader. -if (isWorker) { +// If module is defined, this file is loaded as a CommonJS module. Make sure +// Promise is exported in that case. +if (this.module) { module.exports = Promise; } @@ -685,7 +705,7 @@ this.PromiseWalker = { aPromise[N_INTERNALS].value = aValue; if (aPromise[N_INTERNALS].handlers.length > 0) { this.schedulePromise(aPromise); - } else if (!isWorker && aStatus == STATUS_REJECTED) { + } else if (Cu && aStatus == STATUS_REJECTED) { // This is a rejection and the promise is the last in the chain. // For the time being we therefore have an uncaught error. let id = PendingErrors.register(aValue); @@ -701,10 +721,25 @@ this.PromiseWalker = { scheduleWalkerLoop: function() { this.walkerLoopScheduled = true; - if (isWorker) { - setImmediate(this.walkerLoop); - } else { + + // If this file is loaded on a worker thread, DOMPromise will not behave as + // expected: because native promises are not aware of nested event loops + // created by the debugger, their respective handlers will not be called + // until after leaving the nested event loop. The debugger server relies + // heavily on the use promises, so this could cause the debugger to hang. + // + // To work around this problem, any use of native promises in the debugger + // server should be avoided when it is running on a worker thread. Because + // it is still necessary to be able to schedule runnables on the event + // queue, the worker loader defines the function setImmediate as a + // per-module global for this purpose. + // + // If Cu is defined, this file is loaded on the main thread. Otherwise, it + // is loaded on the worker thread. + if (Cu) { DOMPromise.resolve().then(() => this.walkerLoop()); + } else { + setImmediate(this.walkerLoop); } }, diff --git a/toolkit/modules/Promise.jsm b/toolkit/modules/Promise.jsm index 0996c28433..0df4977aff 100644 --- a/toolkit/modules/Promise.jsm +++ b/toolkit/modules/Promise.jsm @@ -96,17 +96,6 @@ this.Ci = Components.interfaces; this.Cu = Components.utils; this.Cr = Components.results; -// Promise-backend.js can either be loaded as a subscript by this file, or -// required as a CommonJS module by the worker loader. Because certain APIS (in -// particular, Components) are not available in workers, Promise-backend.js -// behaves slightly different in the latter case. -// -// To distinguish between these two cases, the worker loader defines a global -// variable isWorker, and sets it to true. When loading Promise-backend.js as -// a subscript, we need to make sure this variable is defined as well, and set -// it to false. -this.isWorker = false; - this.Cc["@mozilla.org/moz/jssubscript-loader;1"] .getService(this.Ci.mozIJSSubScriptLoader) .loadSubScript("resource://gre/modules/Promise-backend.js", this);