Merge fx-team to m-c a=merge

This commit is contained in:
Wes Kocher 2015-03-27 16:52:38 -07:00
commit 041d4c8544
44 changed files with 669 additions and 108 deletions

View File

@ -46,6 +46,12 @@ searchbar {
-moz-binding: url("chrome://browser/content/search/search.xml#searchbar");
}
/* Prevent shrinking the page content to 0 height and width */
.browserStack > browser {
min-height: 25px;
min-width: 25px;
}
.browserStack > browser {
-moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-browser");
}

View File

@ -32,6 +32,55 @@ function loadURI(tab, url) {
return BrowserTestUtils.browserLoaded(tab.linkedBrowser);
}
// Creates a framescript which caches the current object value from the plugin
// in the page. checkObjectValue below verifies that the framescript is still
// active for the browser and that the cached value matches that from the plugin
// in the page which tells us the plugin hasn't been reinitialized.
function cacheObjectValue(browser) {
let frame_script = function() {
let plugin = content.document.wrappedJSObject.body.firstChild;
let objectValue = plugin.getObjectValue();
addMessageListener("Test:CheckObjectValue", () => {
try {
let plugin = content.document.wrappedJSObject.body.firstChild;
sendAsyncMessage("Test:CheckObjectValue", {
result: plugin.checkObjectValue(objectValue)
});
}
catch (e) {
sendAsyncMessage("Test:CheckObjectValue", {
result: null,
exception: e.toString()
});
}
});
};
browser.messageManager.loadFrameScript("data:,(" + frame_script.toString() + ")();", false)
}
// See the notes for cacheObjectValue above.
function checkObjectValue(browser) {
let mm = browser.messageManager;
return new Promise((resolve, reject) => {
let listener = ({ data }) => {
mm.removeMessageListener("Test:CheckObjectValue", listener);
if (data.result === null) {
ok(false, "checkObjectValue threw an exception: " + data.exception);
reject(data.exception);
}
else {
resolve(data.result);
}
};
mm.addMessageListener("Test:CheckObjectValue", listener);
mm.sendAsyncMessage("Test:CheckObjectValue");
});
}
add_task(function*() {
let embed = '<embed type="application/x-test" allowscriptaccess="always" allowfullscreen="true" wmode="window" width="640" height="480"></embed>'
setTestPluginEnabledState(Ci.nsIPluginTag.STATE_ENABLED);
@ -57,15 +106,12 @@ add_task(function*() {
is(gBrowser.tabs[2], tabs[3], "tab3");
is(gBrowser.tabs[3], tabs[4], "tab4");
let plugin = tabs[4].linkedBrowser.contentDocument.wrappedJSObject.body.firstChild;
let tab4_plugin_object = plugin.getObjectValue();
cacheObjectValue(tabs[4].linkedBrowser);
swapTabsAndCloseOther(3, 2); // now: 0 1 4
gBrowser.selectedTab = gBrowser.tabs[2];
let doc = gBrowser.tabs[2].linkedBrowser.contentDocument.wrappedJSObject;
plugin = doc.body.firstChild;
ok(plugin && plugin.checkObjectValue(tab4_plugin_object), "same plugin instance");
ok((yield checkObjectValue(gBrowser.tabs[2].linkedBrowser)), "same plugin instance");
is(gBrowser.tabs[1], tabs[1], "tab1");
is(gBrowser.tabs[2], tabs[3], "tab4");
@ -77,9 +123,7 @@ add_task(function*() {
swapTabsAndCloseOther(2, 1); // now: 0 4
is(gBrowser.tabs[1], tabs[1], "tab1");
doc = gBrowser.tabs[1].linkedBrowser.contentDocument.wrappedJSObject;
plugin = doc.body.firstChild;
ok(plugin && plugin.checkObjectValue(tab4_plugin_object), "same plugin instance");
ok((yield checkObjectValue(gBrowser.tabs[1].linkedBrowser)), "same plugin instance");
yield clickTest(gBrowser.tabs[1]);

View File

@ -25,12 +25,12 @@ add_task(function*() {
is (nbox.clientWidth, nboxWidth, "Opening the toolbox hasn't changed the width of the nbox");
let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
is (iframe.clientHeight, nboxHeight - 10, "The iframe fits within the available space ");
is (iframe.clientHeight, nboxHeight - 25, "The iframe fits within the available space");
yield toolbox.switchHost(devtools.Toolbox.HostType.SIDE);
iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
iframe.style.minWidth = "1px"; // Disable the min width set in css
is (iframe.clientWidth, nboxWidth - 10, "The iframe fits within the available space");
is (iframe.clientWidth, nboxWidth - 25, "The iframe fits within the available space");
yield cleanup(toolbox);
});

View File

@ -10,6 +10,12 @@ const {Promise: promise} = require("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
/* A host should always allow this much space for the page to be displayed.
* There is also a min-height on the browser, but we still don't want to set
* frame.height to be larger than that, since it can cause problems with
* resizing the toolbox and panel layout. */
const MIN_PAGE_SIZE = 25;
/**
* A toolbox host represents an object that contains a toolbox (e.g. the
* sidebar or a separate window). Any host object should implement the
@ -57,7 +63,7 @@ BottomHost.prototype = {
this.frame.className = "devtools-toolbox-bottom-iframe";
this.frame.height = Math.min(
Services.prefs.getIntPref(this.heightPref),
this._nbox.clientHeight - 10 // Always show at least some page content
this._nbox.clientHeight - MIN_PAGE_SIZE
);
this._nbox.appendChild(this._splitter);
@ -144,7 +150,7 @@ SidebarHost.prototype = {
this.frame.width = Math.min(
Services.prefs.getIntPref(this.widthPref),
this._sidebar.clientWidth - 10 // Always show at least some page content
this._sidebar.clientWidth - MIN_PAGE_SIZE
);
this._sidebar.appendChild(this._splitter);

View File

@ -2328,7 +2328,7 @@ function ElementEditor(aContainer, aNode) {
this.template = this.markup.template.bind(this.markup);
this.doc = this.markup.doc;
this.attrs = {};
this.attrElements = new Map();
this.animationTimers = {};
// The templates will fill the following properties
@ -2408,14 +2408,20 @@ ElementEditor.prototype = {
* Update the state of the editor from the node.
*/
update: function() {
let attrs = this.node.attributes || [];
let attrsToRemove = new Set(this.attrList.querySelectorAll(".attreditor"));
let nodeAttributes = this.node.attributes || [];
// Only loop through the current attributes on the node, anything that's
// been removed will be removed from this DOM because it will be part of
// the attrsToRemove set.
for (let attr of attrs) {
let el = this.attrs[attr.name];
// Keep the data model in sync with attributes on the node.
let currentAttributes = new Set(nodeAttributes.map(a=>a.name));
for (let name of this.attrElements.keys()) {
if (!currentAttributes.has(name)) {
this.removeAttribute(name);
}
}
// Only loop through the current attributes on the node. Missing
// attributes have already been removed at this point.
for (let attr of nodeAttributes) {
let el = this.attrElements.get(attr.name);
let valueChanged = el && el.querySelector(".attr-value").innerHTML !== attr.value;
let isEditing = el && el.querySelector(".editable").inplaceEditor;
let canSimplyShowEditor = el && (!valueChanged || isEditing);
@ -2423,7 +2429,6 @@ ElementEditor.prototype = {
if (canSimplyShowEditor) {
// Element already exists and doesn't need to be recreated.
// Just show it (it's hidden by default due to the template).
attrsToRemove.delete(el);
el.style.removeProperty("display");
} else {
// Create a new editor, because the value of an existing attribute
@ -2439,10 +2444,6 @@ ElementEditor.prototype = {
}
}
}
for (let el of attrsToRemove) {
el.remove();
}
},
_startModifyingAttributes: function() {
@ -2459,6 +2460,18 @@ ElementEditor.prototype = {
".attreditor[data-attr=" + attrName + "] .attr-value");
},
/**
* Remove an attribute from the attrElements object and the DOM
* @param string attrName The name of the attribute to remove
*/
removeAttribute: function(attrName) {
let attr = this.attrElements.get(attrName);
if (attr) {
this.attrElements.delete(attrName);
attr.remove();
}
},
_createAttribute: function(aAttr, aBefore = null) {
// Create the template editor, which will save some variables here.
let data = {
@ -2541,18 +2554,13 @@ ElementEditor.prototype = {
if (aAttr.name == "id") {
before = this.attrList.firstChild;
} else if (aAttr.name == "class") {
let idNode = this.attrs["id"];
let idNode = this.attrElements.get("id");
before = idNode ? idNode.nextSibling : this.attrList.firstChild;
}
this.attrList.insertBefore(attr, before);
// Remove the old version of this attribute from the DOM.
let oldAttr = this.attrs[aAttr.name];
if (oldAttr && oldAttr.parentNode) {
oldAttr.parentNode.removeChild(oldAttr);
}
this.attrs[aAttr.name] = attr;
this.removeAttribute(aAttr.name);
this.attrElements.set(aAttr.name, attr);
let collapsedValue;
if (aAttr.value.match(COLLAPSE_DATA_URL_REGEX)) {

View File

@ -140,7 +140,7 @@ function* checkData(index, editor, inspector) {
} else {
let nodeFront = yield getNodeFront("#node14", inspector);
let editor = getContainerForNodeFront(nodeFront, inspector).editor;
let attr = editor.attrs["style"].querySelector(".editable");
let attr = editor.attrElements.get("style").querySelector(".editable");
is(attr.textContent, completion, "Correct value is persisted after pressing Enter");
}
}

View File

@ -39,6 +39,32 @@ const TEST_DATA = [
}), "newattr attribute removed");
}
},
{
desc: "Re-adding an attribute",
test: () => {
let node1 = getNode("#node1");
node1.setAttribute("newattr", "newattrval");
},
check: function*(inspector) {
let {editor} = yield getContainerForSelector("#node1", inspector);
ok([...editor.attrList.querySelectorAll(".attreditor")].some(attr => {
return attr.textContent.trim() === "newattr=\"newattrval\"";
}), "newattr attribute found");
}
},
{
desc: "Changing an attribute",
test: () => {
let node1 = getNode("#node1");
node1.setAttribute("newattr", "newattrchanged");
},
check: function*(inspector) {
let {editor} = yield getContainerForSelector("#node1", inspector);
ok([...editor.attrList.querySelectorAll(".attreditor")].some(attr => {
return attr.textContent.trim() === "newattr=\"newattrchanged\"";
}), "newattr attribute found");
}
},
{
desc: "Updating the text-content",
test: () => {

View File

@ -123,7 +123,7 @@ function* assertNodeFlashing(nodeFront, inspector) {
function* assertAttributeFlashing(nodeFront, attribute, inspector) {
let container = getContainerForNodeFront(nodeFront, inspector);
ok(container, "Markup container for node found");
ok(container.editor.attrs[attribute], "Attribute exists on editor");
ok(container.editor.attrElements.get(attribute), "Attribute exists on editor");
let attributeElement = container.editor.getAttributeElement(attribute);

View File

@ -22,7 +22,7 @@ add_task(function*() {
info("Focus the ID attribute and change its content");
let {editor} = yield getContainerForSelector("#test-div", inspector);
let attr = editor.attrs["id"].querySelector(".editable");
let attr = editor.attrElements.get("id").querySelector(".editable");
let mutated = inspector.once("markupmutation");
setEditableFieldValue(attr,
attr.textContent + ' class="newclass" style="color:green"', inspector);

View File

@ -47,7 +47,7 @@ let TEST_DATA = [{
},
validate: (element, container, inspector) => {
let editor = container.editor;
let visibleAttrText = editor.attrs["style"].querySelector(".attr-value").textContent;
let visibleAttrText = editor.attrElements.get("style").querySelector(".attr-value").textContent;
is (visibleAttrText, DATA_URL_INLINE_STYLE_COLLAPSED);
}
}, {
@ -58,7 +58,7 @@ let TEST_DATA = [{
},
validate: (element, container, inspector) => {
let editor = container.editor;
let visibleAttrText = editor.attrs["data-long"].querySelector(".attr-value").textContent;
let visibleAttrText = editor.attrElements.get("data-long").querySelector(".attr-value").textContent;
is (visibleAttrText, LONG_ATTRIBUTE_COLLAPSED)
}
}, {
@ -69,7 +69,7 @@ let TEST_DATA = [{
},
validate: (element, container, inspector) => {
let editor = container.editor;
let visibleAttrText = editor.attrs["src"].querySelector(".attr-value").textContent;
let visibleAttrText = editor.attrElements.get("src").querySelector(".attr-value").textContent;
is (visibleAttrText, DATA_URL_ATTRIBUTE_COLLAPSED);
}
}];

View File

@ -36,7 +36,7 @@ function* testCollapsedLongAttribute(inspector) {
});
let {editor} = yield getContainerForSelector("#node24", inspector);
let attr = editor.attrs["data-long"].querySelector(".editable");
let attr = editor.attrElements.get("data-long").querySelector(".editable");
// Check to make sure it has expanded after focus
attr.focus();
@ -48,7 +48,7 @@ function* testCollapsedLongAttribute(inspector) {
setEditableFieldValue(attr, input.value + ' data-short="ABC"', inspector);
yield inspector.once("markupmutation");
let visibleAttrText = editor.attrs["data-long"].querySelector(".attr-value").textContent;
let visibleAttrText = editor.attrElements.get("data-long").querySelector(".attr-value").textContent;
is (visibleAttrText, LONG_ATTRIBUTE_COLLAPSED)
yield assertAttributes("#node24", {
@ -69,7 +69,7 @@ function* testModifyInlineStyleWithQuotes(inspector) {
let onMutated = inspector.once("markupmutation");
let {editor} = yield getContainerForSelector("#node26", inspector);
let attr = editor.attrs["style"].querySelector(".editable");
let attr = editor.attrElements.get("style").querySelector(".editable");
attr.focus();
EventUtils.sendKey("return", inspector.panelWin);
@ -105,7 +105,7 @@ function* testEditingAttributeWithMixedQuotes(inspector) {
let onMutated = inspector.once("markupmutation");
let {editor} = yield getContainerForSelector("#node27", inspector);
let attr = editor.attrs["class"].querySelector(".editable");
let attr = editor.attrElements.get("class").querySelector(".editable");
attr.focus();
EventUtils.sendKey("return", inspector.panelWin);

View File

@ -27,7 +27,7 @@ function* testWellformedMixedCase(inspector) {
info("Focusing the viewBox attribute editor");
let {editor} = yield getContainerForSelector("svg", inspector);
let attr = editor.attrs["viewBox"].querySelector(".editable");
let attr = editor.attrElements.get("viewBox").querySelector(".editable");
attr.focus();
EventUtils.sendKey("return", inspector.panelWin);
@ -53,7 +53,7 @@ function* testMalformedMixedCase(inspector) {
info("Focusing the viewBox attribute editor");
let {editor} = yield getContainerForSelector("svg", inspector);
let attr = editor.attrs["viewBox"].querySelector(".editable");
let attr = editor.attrElements.get("viewBox").querySelector(".editable");
attr.focus();
EventUtils.sendKey("return", inspector.panelWin);

View File

@ -133,7 +133,7 @@ function* runEditAttributesTest(test, inspector) {
info("Listening for the markupmutation event");
let nodeMutated = inspector.once("markupmutation");
let attr = container.editor.attrs[test.name].querySelector(".editable");
let attr = container.editor.attrElements.get(test.name).querySelector(".editable");
setEditableFieldValue(attr, test.value, inspector);
yield nodeMutated;

View File

@ -57,7 +57,6 @@ skip-if = e10s # Bug 1091596
[browser_net_cyrillic-02.js]
[browser_net_details-no-duplicated-content.js]
[browser_net_filter-01.js]
skip-if = e10s # Bug 1091603
[browser_net_filter-02.js]
[browser_net_filter-03.js]
[browser_net_filter-04.js]

View File

@ -297,8 +297,10 @@ PerformanceFront.prototype = {
return profilerStatus.currentTime;
}
// Extend the profiler options so that protocol.js doesn't modify the original.
let profilerOptions = extend({}, this._customProfilerOptions);
// If this._customProfilerOptions is defined, use those to pass in
// to the profiler actor. The profiler actor handles all the defaults
// now, so this should only be used for tests.
let profilerOptions = this._customProfilerOptions || {};
yield this._request("profiler", "startProfiler", profilerOptions);
this.emit("profiler-activated");
@ -400,19 +402,6 @@ PerformanceFront.prototype = {
deferred.resolve();
}),
/**
* Overrides the options sent to the built-in profiler module when activating,
* such as the maximum entries count, the sampling interval etc.
*
* Used in tests and for older backend implementations.
*/
_customProfilerOptions: {
entries: 1000000,
interval: 1,
features: ["js"],
threadFilters: ["GeckoMain"]
},
/**
* Returns an object indicating if mock actors are being used or not.
*/

View File

@ -33,9 +33,9 @@ public:
// Check whether two markers should be considered the same,
// for the purpose of pairing start and end markers. Normally
// this definition suffices.
virtual bool Equals(const TimelineMarker* aOther)
virtual bool Equals(const TimelineMarker& aOther)
{
return strcmp(mName, aOther->mName) == 0;
return strcmp(mName, aOther.mName) == 0;
}
// Add details specific to this marker type to aMarker. The

View File

@ -2977,10 +2977,10 @@ nsDocShell::PopProfileTimelineMarkers(
// If we see an unpaired START, we keep it around for the next call
// to PopProfileTimelineMarkers. We store the kept START objects in
// this array.
nsTArray<TimelineMarker*> keptMarkers;
nsTArray<UniquePtr<TimelineMarker>> keptMarkers;
for (uint32_t i = 0; i < mProfileTimelineMarkers.Length(); ++i) {
TimelineMarker* startPayload = mProfileTimelineMarkers[i];
UniquePtr<TimelineMarker>& startPayload = mProfileTimelineMarkers[i];
const char* startMarkerName = startPayload->GetName();
bool hasSeenPaintedLayer = false;
@ -3002,7 +3002,7 @@ nsDocShell::PopProfileTimelineMarkers(
// enough for the amount of markers to always be small enough that the
// nested for loop isn't going to be a performance problem.
for (uint32_t j = i + 1; j < mProfileTimelineMarkers.Length(); ++j) {
TimelineMarker* endPayload = mProfileTimelineMarkers[j];
UniquePtr<TimelineMarker>& endPayload = mProfileTimelineMarkers[j];
const char* endMarkerName = endPayload->GetName();
// Look for Layer markers to stream out paint markers.
@ -3011,7 +3011,7 @@ nsDocShell::PopProfileTimelineMarkers(
endPayload->AddLayerRectangles(layerRectangles);
}
if (!startPayload->Equals(endPayload)) {
if (!startPayload->Equals(*endPayload)) {
continue;
}
@ -3048,14 +3048,13 @@ nsDocShell::PopProfileTimelineMarkers(
// If we did not see the corresponding END, keep the START.
if (!hasSeenEnd) {
keptMarkers.AppendElement(mProfileTimelineMarkers[i]);
keptMarkers.AppendElement(Move(mProfileTimelineMarkers[i]));
mProfileTimelineMarkers.RemoveElementAt(i);
--i;
}
}
}
ClearProfileTimelineMarkers();
mProfileTimelineMarkers.SwapElements(keptMarkers);
if (!ToJSValue(aCx, profileTimelineMarkers, aProfileTimelineMarkers)) {
@ -3086,10 +3085,10 @@ nsDocShell::AddProfileTimelineMarker(const char* aName,
}
void
nsDocShell::AddProfileTimelineMarker(UniquePtr<TimelineMarker>& aMarker)
nsDocShell::AddProfileTimelineMarker(UniquePtr<TimelineMarker>&& aMarker)
{
if (mProfileTimelineRecording) {
mProfileTimelineMarkers.AppendElement(aMarker.release());
mProfileTimelineMarkers.AppendElement(Move(aMarker));
}
}
@ -3125,9 +3124,6 @@ nsDocShell::GetWindowDraggingAllowed(bool* aValue)
void
nsDocShell::ClearProfileTimelineMarkers()
{
for (uint32_t i = 0; i < mProfileTimelineMarkers.Length(); ++i) {
delete mProfileTimelineMarkers[i];
}
mProfileTimelineMarkers.Clear();
}

View File

@ -272,7 +272,7 @@ public:
// See nsIDocShell::recordProfileTimelineMarkers
void AddProfileTimelineMarker(const char* aName,
TracingMetadata aMetaData);
void AddProfileTimelineMarker(mozilla::UniquePtr<TimelineMarker>& aMarker);
void AddProfileTimelineMarker(mozilla::UniquePtr<TimelineMarker>&& aMarker);
// Global counter for how many docShells are currently recording profile
// timeline markers
@ -984,7 +984,7 @@ private:
// True if recording profiles.
bool mProfileTimelineRecording;
nsTArray<TimelineMarker*> mProfileTimelineMarkers;
nsTArray<mozilla::UniquePtr<TimelineMarker>> mProfileTimelineMarkers;
// Get rid of all the timeline markers accumulated so far
void ClearProfileTimelineMarkers();

View File

@ -934,13 +934,13 @@ public:
}
}
virtual bool Equals(const TimelineMarker* aOther) override
virtual bool Equals(const TimelineMarker& aOther) override
{
if (!TimelineMarker::Equals(aOther)) {
return false;
}
// Console markers must have matching causes as well.
return GetCause() == aOther->GetCause();
return GetCause() == aOther.GetCause();
}
virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker) override
@ -1057,7 +1057,7 @@ Console::Method(JSContext* aCx, MethodName aMethodName,
MakeUnique<ConsoleTimelineMarker>(docShell,
aMethodName == MethodTime ? TRACING_INTERVAL_START : TRACING_INTERVAL_END,
key);
docShell->AddProfileTimelineMarker(marker);
docShell->AddProfileTimelineMarker(Move(marker));
}
}
}

View File

@ -312,7 +312,6 @@ AutoJSAPI::~AutoJSAPI()
{
if (mOwnErrorReporting) {
MOZ_ASSERT(NS_IsMainThread(), "See corresponding assertion in TakeOwnershipOfErrorReporting()");
JS::ContextOptionsRef(cx()).setAutoJSAPIOwnsErrorReporting(mOldAutoJSAPIOwnsErrorReporting);
if (HasException()) {
@ -342,6 +341,13 @@ AutoJSAPI::~AutoJSAPI()
NS_WARNING("OOMed while acquiring uncaught exception from JSAPI");
}
}
// We need to do this _after_ processing the existing exception, because the
// JS engine can throw while doing that, and uses this bit to determine what
// to do in that case: squelch the exception if the bit is set, otherwise
// call the error reporter. Calling WarningOnlyErrorReporter with a
// non-warning will assert, so we need to make sure we do the former.
JS::ContextOptionsRef(cx()).setAutoJSAPIOwnsErrorReporting(mOldAutoJSAPIOwnsErrorReporting);
}
if (mOldErrorReporter.isSome()) {

View File

@ -1094,7 +1094,7 @@ EventListenerManager::HandleEventInternal(nsPresContext* aPresContext,
mozilla::UniquePtr<TimelineMarker> marker =
MakeUnique<EventTimelineMarker>(ds, TRACING_INTERVAL_START,
phase, typeStr);
ds->AddProfileTimelineMarker(marker);
ds->AddProfileTimelineMarker(Move(marker));
}
}

View File

@ -1820,7 +1820,7 @@ _popupcontextmenu(NPP instance, NPMenu* menu)
if (success) {
return mozilla::plugins::PluginUtilsOSX::ShowCocoaContextMenu(menu,
screenX, screenY,
PluginModuleChild::GetChrome(),
InstCast(instance)->Manager(),
ProcessBrowserEvents);
} else {
NS_WARNING("Convertpoint failed, could not created contextmenu.");

View File

@ -229,6 +229,13 @@ ReportError(JSContext *cx, const char *message, JSErrorReport *reportp,
if (cx->options().autoJSAPIOwnsErrorReporting() || JS_IsRunning(cx)) {
if (ErrorToException(cx, message, reportp, callback, userRef))
return;
/*
* The AutoJSAPI error reporter only allows warnings to be reported so
* just ignore this error rather than try to report it.
*/
if (cx->options().autoJSAPIOwnsErrorReporting())
return;
}
/*

View File

@ -608,6 +608,12 @@ js::ErrorToException(JSContext *cx, const char *message, JSErrorReport *reportp,
static bool
IsDuckTypedErrorObject(JSContext *cx, HandleObject exnObject, const char **filename_strp)
{
/*
* This function is called from ErrorReport::init and so should not generate
* any new exceptions.
*/
AutoClearPendingException acpe(cx);
bool found;
if (!JS_HasProperty(cx, exnObject, js_message_str, &found) || !found)
return false;

View File

@ -115,6 +115,20 @@ ExnTypeFromProtoKey(JSProtoKey key)
return type;
}
class AutoClearPendingException
{
JSContext *cx;
public:
explicit AutoClearPendingException(JSContext *cxArg)
: cx(cxArg)
{ }
~AutoClearPendingException() {
cx->clearPendingException();
}
};
} // namespace js
#endif /* jsexn_h */

View File

@ -4936,7 +4936,7 @@ FrameLayerBuilder::DrawPaintedLayer(PaintedLayer* aLayer,
if (isRecording) {
mozilla::UniquePtr<TimelineMarker> marker =
MakeUnique<LayerTimelineMarker>(docShell, aRegionToDraw);
docShell->AddProfileTimelineMarker(marker);
docShell->AddProfileTimelineMarker(Move(marker));
}
}

View File

@ -48,6 +48,7 @@ public class LocalReadingListStorage implements ReadingListStorage {
* These are not common: they should only occur if a conflict occurs.
*/
private final Queue<ClientReadingListRecord> deletions;
private final Queue<String> deletedGUIDs;
/**
* These are additions or changes fetched from the server.
@ -63,16 +64,17 @@ public class LocalReadingListStorage implements ReadingListStorage {
LocalReadingListChangeAccumulator() {
this.changes = new ConcurrentLinkedQueue<>();
this.deletions = new ConcurrentLinkedQueue<>();
this.deletedGUIDs = new ConcurrentLinkedQueue<>();
this.additionsOrChanges = new ConcurrentLinkedQueue<>();
}
public boolean flushDeletions() throws RemoteException {
if (deletions.isEmpty()) {
if (deletions.isEmpty() && deletedGUIDs.isEmpty()) {
return true;
}
long[] ids = new long[deletions.size()];
String[] guids = new String[deletions.size()];
String[] guids = new String[deletions.size() + deletedGUIDs.size()];
int iID = 0;
int iGUID = 0;
for (ClientReadingListRecord record : deletions) {
@ -86,6 +88,9 @@ public class LocalReadingListStorage implements ReadingListStorage {
guids[iGUID++] = guid;
}
}
for (String guid : deletedGUIDs) {
guids[iGUID++] = guid;
}
if (iID > 0) {
client.delete(URI_WITH_DELETED, RepoUtils.computeSQLLongInClause(ids, ReadingListItems._ID), null);
@ -96,6 +101,7 @@ public class LocalReadingListStorage implements ReadingListStorage {
}
deletions.clear();
deletedGUIDs.clear();
return true;
}
@ -211,6 +217,11 @@ public class LocalReadingListStorage implements ReadingListStorage {
deletions.add(record);
}
@Override
public void addDeletion(String guid) {
deletedGUIDs.add(guid);
}
@Override
public void addChangedRecord(ClientReadingListRecord record) {
changes.add(record);
@ -318,6 +329,20 @@ public class LocalReadingListStorage implements ReadingListStorage {
}
}
@Override
public Cursor getDeletedItems() {
final String[] projection = new String[] {
ReadingListItems.GUID,
};
final String selection = "(" + ReadingListItems.IS_DELETED + " = 1) AND (" + ReadingListItems.GUID + " IS NOT NULL)";
try {
return client.query(URI_WITH_DELETED, projection, selection, null, null);
} catch (RemoteException e) {
throw new IllegalStateException(e);
}
}
@Override
public Cursor getNew() {
// N.B., query for items that have no GUID, regardless of status.

View File

@ -11,6 +11,7 @@ package org.mozilla.gecko.reading;
* via UPDATE) to the DB.
*/
public interface ReadingListChangeAccumulator {
void addDeletion(String guid);
void addDeletion(ClientReadingListRecord record);
void addChangedRecord(ClientReadingListRecord record);
void addUploadedRecord(ClientReadingListRecord up, ServerReadingListRecord down);

View File

@ -431,6 +431,76 @@ public class ReadingListClient {
}
}
private class DeleteBatchingDelegate implements ReadingListDeleteDelegate {
private final Queue<String> queue;
private final ReadingListDeleteDelegate batchDeleteDelegate;
private final Executor executor;
DeleteBatchingDelegate(Queue<String> guids,
ReadingListDeleteDelegate batchDeleteDelegate,
Executor executor) {
this.queue = guids;
this.batchDeleteDelegate = batchDeleteDelegate;
this.executor = executor;
}
void next() {
final String guid = queue.poll();
executor.execute(new Runnable() {
@Override
public void run() {
if (guid == null) {
batchDeleteDelegate.onBatchDone();
return;
}
again(guid);
}
});
}
void again(String guid) {
delete(guid, DeleteBatchingDelegate.this, -1L);
}
@Override
public void onSuccess(ReadingListRecordResponse response,
ReadingListRecord record) {
batchDeleteDelegate.onSuccess(response, record);
next();
}
@Override
public void onPreconditionFailed(String guid, MozResponse response) {
batchDeleteDelegate.onPreconditionFailed(guid, response);
next();
}
@Override
public void onRecordMissingOrDeleted(String guid, MozResponse response) {
batchDeleteDelegate.onRecordMissingOrDeleted(guid, response);
next();
}
@Override
public void onFailure(Exception e) {
batchDeleteDelegate.onFailure(e);
next();
}
@Override
public void onFailure(MozResponse response) {
batchDeleteDelegate.onFailure(response);
next();
}
@Override
public void onBatchDone() {
// This should never occur, but if it does, pass through.
batchDeleteDelegate.onBatchDone();
}
}
// Deliberately declare `delegate` non-final so we can't capture it below. We prefer
// to use `recordDelegate` explicitly.
public void getOne(final String guid, ReadingListRecordDelegate delegate, final long ifModifiedSince) {
@ -511,6 +581,17 @@ public class ReadingListClient {
r.post(body);
}
public void delete(final Queue<String> guids, final Executor executor, final ReadingListDeleteDelegate batchDeleteDelegate) {
if (guids.isEmpty()) {
batchDeleteDelegate.onBatchDone();
return;
}
final ReadingListDeleteDelegate deleteDelegate = new DeleteBatchingDelegate(guids, batchDeleteDelegate, executor);
delete(guids.poll(), deleteDelegate, -1L);
}
public void delete(final String guid, final ReadingListDeleteDelegate delegate, final long ifUnmodifiedSince) {
final BaseResource r = getRelativeArticleResource(guid);

View File

@ -10,9 +10,9 @@ import org.mozilla.gecko.reading.ReadingListRecord.ServerMetadata;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import android.annotation.TargetApi;
import android.database.AbstractWindowedCursor;
import android.database.Cursor;
import android.database.CursorWindow;
import android.database.sqlite.SQLiteCursor;
import android.os.Build;
/**
@ -132,11 +132,11 @@ public class ReadingListClientRecordFactory {
@SuppressWarnings("deprecation")
private final void fillGingerbread(ExtendedJSONObject o, Cursor c, String f, int i) {
if (!(c instanceof SQLiteCursor)) {
if (!(c instanceof AbstractWindowedCursor)) {
throw new IllegalStateException("Unable to handle cursors that don't have a CursorWindow!");
}
final SQLiteCursor sqc = (SQLiteCursor) c;
final AbstractWindowedCursor sqc = (AbstractWindowedCursor) c;
final CursorWindow w = sqc.getWindow();
final int pos = c.getPosition();
if (w.isNull(pos, i)) {

View File

@ -8,7 +8,8 @@ import org.mozilla.gecko.sync.net.MozResponse;
/**
* Response delegate for a server DELETE.
* Only one of these methods will be called, and it will be called precisely once.
* Only one of these methods will be called, and it will be called precisely once,
* unless batching is used.
*/
public interface ReadingListDeleteDelegate {
void onSuccess(ReadingListRecordResponse response, ReadingListRecord record);
@ -16,4 +17,5 @@ public interface ReadingListDeleteDelegate {
void onRecordMissingOrDeleted(String guid, MozResponse response);
void onFailure(Exception e);
void onFailure(MozResponse response);
void onBatchDone();
}

View File

@ -8,6 +8,7 @@ import android.database.Cursor;
public interface ReadingListStorage {
Cursor getModified();
Cursor getDeletedItems();
Cursor getStatusChanges();
Cursor getNew();
Cursor getAll();

View File

@ -81,6 +81,12 @@ public class ReadingListSyncAdapter extends AbstractThreadedSyncAdapter {
syncDelegate.handleError(e);
}
@Override
public void onDeletionsUploadComplete() {
Logger.debug(LOG_TAG, "Step: onDeletionsUploadComplete");
this.result.stats.numEntries += 1; // TODO: Bug 1140809.
}
@Override
public void onStatusUploadComplete(Collection<String> uploaded,
Collection<String> failed) {

View File

@ -176,6 +176,80 @@ public class ReadingListSynchronizer {
}
}
private static class DeletionUploadDelegate implements ReadingListDeleteDelegate {
private final ReadingListChangeAccumulator acc;
private final StageDelegate next;
DeletionUploadDelegate(ReadingListChangeAccumulator acc, StageDelegate next) {
this.acc = acc;
this.next = next;
}
@Override
public void onBatchDone() {
try {
acc.finish();
} catch (Exception e) {
next.fail(e);
return;
}
next.next();
}
@Override
public void onSuccess(ReadingListRecordResponse response,
ReadingListRecord record) {
Logger.debug(LOG_TAG, "Tracking uploaded deletion " + record.getGUID());
acc.addDeletion(record.getGUID());
}
@Override
public void onPreconditionFailed(String guid, MozResponse response) {
// Should never happen.
}
@Override
public void onRecordMissingOrDeleted(String guid, MozResponse response) {
// Great!
Logger.debug(LOG_TAG, "Tracking redundant deletion " + guid);
acc.addDeletion(guid);
}
@Override
public void onFailure(Exception e) {
// Ignore.
}
@Override
public void onFailure(MozResponse response) {
// Ignore.
}
}
private Queue<String> collectDeletedIDsFromCursor(Cursor cursor) {
try {
final Queue<String> toDelete = new LinkedList<>();
final int columnGUID = cursor.getColumnIndexOrThrow(ReadingListItems.GUID);
while (cursor.moveToNext()) {
final String guid = cursor.getString(columnGUID);
if (guid == null) {
// Nothing we can do here.
continue;
}
toDelete.add(guid);
}
return toDelete;
} finally {
cursor.close();
}
}
private static class StatusUploadDelegate implements ReadingListRecordUploadDelegate {
private final ReadingListChangeAccumulator acc;
@ -462,6 +536,36 @@ public class ReadingListSynchronizer {
}
}
protected void uploadDeletions(final StageDelegate delegate) {
try {
final Cursor cursor = local.getDeletedItems();
if (cursor == null) {
delegate.fail(new RuntimeException("Unable to get unread item cursor."));
return;
}
final Queue<String> toDelete = collectDeletedIDsFromCursor(cursor);
// Nothing to do.
if (toDelete.isEmpty()) {
Logger.debug(LOG_TAG, "No new deletions to upload. Skipping.");
delegate.next();
return;
} else {
Logger.debug(LOG_TAG, "Deleting " + toDelete.size() + " records from the server.");
}
final ReadingListChangeAccumulator acc = this.local.getChangeAccumulator();
final DeletionUploadDelegate deleteDelegate = new DeletionUploadDelegate(acc, delegate);
// Don't send I-U-S; we're happy for the client to win, because this is a one-way state change.
this.remote.delete(toDelete, executor, deleteDelegate);
} catch (Exception e) {
delegate.fail(e);
}
}
// N.B., status changes for items that haven't been uploaded yet are dealt with in
// uploadNewItems.
protected void uploadUnreadChanges(final StageDelegate delegate) {
@ -697,13 +801,13 @@ public class ReadingListSynchronizer {
}
/**
* Upload unread changes, then upload new items, then call `done`.
* Upload deletions and unread changes, then upload new items, then call `done`.
* Substantially modified records are uploaded last.
*
* @param syncDelegate only used for status callbacks.
*/
private void syncUp(final ReadingListSynchronizerDelegate syncDelegate, final StageDelegate done) {
// Second.
// Third.
final StageDelegate onNewItemsUploaded = new NextDelegate(executor) {
@Override
public void doNext() {
@ -717,7 +821,7 @@ public class ReadingListSynchronizer {
}
};
// First.
// Second.
final StageDelegate onUnreadChangesUploaded = new NextDelegate(executor) {
@Override
public void doNext() {
@ -732,8 +836,23 @@ public class ReadingListSynchronizer {
}
};
// First.
final StageDelegate onDeletionsUploaded = new NextDelegate(executor) {
@Override
public void doNext() {
syncDelegate.onDeletionsUploadComplete();
uploadUnreadChanges(onUnreadChangesUploaded);
}
@Override
public void doFail(Exception e) {
Logger.warn(LOG_TAG, "Uploading deletions failed.", e);
done.fail(e);
}
};
try {
uploadUnreadChanges(onUnreadChangesUploaded);
uploadDeletions(onDeletionsUploaded);
} catch (Exception ee) {
done.fail(ee);
}

View File

@ -12,6 +12,7 @@ public interface ReadingListSynchronizerDelegate {
// These are called sequentially, or not at all
// if a failure occurs.
void onDeletionsUploadComplete();
void onStatusUploadComplete(Collection<String> uploaded, Collection<String> failed);
void onNewItemUploadComplete(Collection<String> uploaded, Collection<String> failed);
void onDownloadComplete();

View File

@ -2406,7 +2406,29 @@ var WalkerActor = protocol.ActorClass({
this._orphaned = new Set();
}
return pending;
// Clear out any duplicate attribute mutations before sending them over
// the protocol. Keep only the most recent change for each attribute.
let targetMap = {};
let filtered = pending.reverse().filter(mutation => {
if (mutation.type === "attributes") {
if (!targetMap[mutation.target]) {
targetMap[mutation.target] = {};
}
let attributesForTarget = targetMap[mutation.target];
if (attributesForTarget[mutation.attributeName]) {
// Since the array was reversed, if we've seen this attribute already
// then this one is a duplicate and can be skipped.
return false;
}
attributesForTarget[mutation.attributeName] = true;
}
return true;
}).reverse();
return filtered;
}, {
request: {
cleanup: Option(0)

View File

@ -7,7 +7,7 @@ const {Cc, Ci, Cu, Cr} = require("chrome");
const Services = require("Services");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils.js");
let DEFAULT_PROFILER_ENTRIES = 1000000;
let DEFAULT_PROFILER_ENTRIES = 10000000;
let DEFAULT_PROFILER_INTERVAL = 1;
let DEFAULT_PROFILER_FEATURES = ["js"];
let DEFAULT_PROFILER_THREADFILTERS = ["GeckoMain"];

View File

@ -71,6 +71,7 @@ function WebConsoleActor(aConnection, aParentActor)
this._netEvents = new Map();
this._gripDepth = 0;
this._listeners = new Set();
this._lastConsoleInputEvaluation = undefined;
this._onWillNavigate = this._onWillNavigate.bind(this);
this._onChangedToplevelDocument = this._onChangedToplevelDocument.bind(this);
@ -358,6 +359,7 @@ WebConsoleActor.prototype =
this._actorPool = null;
this._jstermHelpersCache = null;
this._lastConsoleInputEvaluation = null;
this._evalWindow = null;
this._netEvents.clear();
this.dbg.enabled = false;
@ -503,6 +505,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.
//////////////////
@ -816,6 +829,8 @@ WebConsoleActor.prototype =
errorMessage = e;
}
this._lastConsoleInputEvaluation = result;
return {
from: this.actorID,
input: input,

View File

@ -45,10 +45,12 @@ addTest(setupAttrTest);
addTest(testAddAttribute);
addTest(testChangeAttribute);
addTest(testRemoveAttribute);
addTest(testQueuedMutations);
addTest(setupFrameAttrTest);
addTest(testAddAttribute);
addTest(testChangeAttribute);
addTest(testRemoveAttribute);
addTest(testQueuedMutations);
function setupAttrTest() {
attrNode = gInspectee.querySelector("#a")
@ -85,10 +87,13 @@ function testAddAttribute() {
}
function testChangeAttribute() {
attrNode.setAttribute("data-newattr", "changedvalue");
gWalker.once("mutations", () => {
attrNode.setAttribute("data-newattr", "changedvalue1");
attrNode.setAttribute("data-newattr", "changedvalue2");
attrNode.setAttribute("data-newattr", "changedvalue3");
gWalker.once("mutations", mutations => {
is(mutations.length, 1, "Only one mutation is sent for multiple queued attribute changes");
is(attrFront.attributes.length, 3, "Should have id and two new attributes.");
is(attrFront.getAttribute("data-newattr"), "changedvalue", "Node front should have the changed first value");
is(attrFront.getAttribute("data-newattr"), "changedvalue3", "Node front should have the changed first value");
is(attrFront.getAttribute("data-newattr2"), "newvalue", "Second value should remain unchanged.");
runNextTest();
});
@ -98,12 +103,44 @@ function testRemoveAttribute() {
attrNode.removeAttribute("data-newattr2");
gWalker.once("mutations", () => {
is(attrFront.attributes.length, 2, "Should have id and one remaining attribute.");
is(attrFront.getAttribute("data-newattr"), "changedvalue", "Node front should still have the first value");
is(attrFront.getAttribute("data-newattr"), "changedvalue3", "Node front should still have the first value");
ok(!attrFront.hasAttribute("data-newattr2"), "Second value should be removed.");
runNextTest();
})
}
function testQueuedMutations() {
// All modifications to each attribute should be queued in one mutation event.
attrNode.removeAttribute("data-newattr");
attrNode.setAttribute("data-newattr", "1");
attrNode.removeAttribute("data-newattr");
attrNode.setAttribute("data-newattr", "2");
attrNode.removeAttribute("data-newattr");
for (var i = 0; i <= 1000; i++) {
attrNode.setAttribute("data-newattr2", i);
}
attrNode.removeAttribute("data-newattr3");
attrNode.setAttribute("data-newattr3", "1");
attrNode.removeAttribute("data-newattr3");
attrNode.setAttribute("data-newattr3", "2");
attrNode.removeAttribute("data-newattr3");
attrNode.setAttribute("data-newattr3", "3");
gWalker.once("mutations", mutations => {
is(mutations.length, 3, "Only one mutation each is sent for multiple queued attribute changes");
is(attrFront.attributes.length, 3, "Should have id, data-newattr2, and data-newattr3.");
is(attrFront.getAttribute("data-newattr2"), "1000", "Node front should still have the correct value");
is(attrFront.getAttribute("data-newattr3"), "3", "Node front should still have the correct value");
ok(!attrFront.hasAttribute("data-newattr"), "Attribute value should be removed.");
runNextTest();
})
}
addTest(function cleanup() {
delete gInspectee;
delete gWalker;

View File

@ -753,13 +753,8 @@ NetworkMonitor.prototype = {
event.private = httpActivity.private;
// Determine if this is an XHR request.
try {
let callbacks = aChannel.notificationCallbacks;
let xhrRequest = callbacks ? callbacks.getInterface(Ci.nsIXMLHttpRequest) : null;
httpActivity.isXHR = event.isXHR = !!xhrRequest;
} catch (e) {
httpActivity.isXHR = event.isXHR = false;
}
httpActivity.isXHR = event.isXHR =
(aChannel.loadInfo.contentPolicyType === Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST);
// Determine the HTTP version.
aChannel.QueryInterface(Ci.nsIHttpChannelInternal);

View File

@ -15,6 +15,8 @@ support-files =
[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]
@ -26,4 +28,3 @@ support-files =
[test_object_actor_native_getters_lenient_this.html]
[test_page_errors.html]
[test_throw.html]
[test_jsterm_cd_iframe.html]

View File

@ -8,6 +8,10 @@ const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
const XHTML_NS = "http://www.w3.org/1999/xhtml";
Cu.import("resource://gre/modules/Services.jsm");
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
// This gives logging to stdout for tests
var {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
let devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
let WebConsoleUtils = devtools.require("devtools/toolkit/webconsole/utils").Utils;

View File

@ -0,0 +1,130 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf8">
<title>Test for the $_ getter</title>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript;version=1.8" src="common.js"></script>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>Test for the $_ getter</p>
<iframe id="content-iframe" src="http://example.com/chrome/toolkit/devtools/webconsole/test/sandboxed_iframe.html"></iframe>
<script class="testbody" type="text/javascript;version=1.8">
SimpleTest.waitForExplicitFinish();
let gState;
function evaluateJS(input, callback) {
return new Promise((resolve, reject) => {
gState.client.evaluateJSAsync(input, response => {
if (callback) {
callback(response);
}
resolve(response);
});
});
}
function startTest()
{
removeEventListener("load", startTest);
attachConsole([], state => {
gState = state;
let tests = [checkUndefinedResult,checkAdditionResult,checkObjectResult];
runTests(tests, testEnd);
}, true);
}
let checkUndefinedResult = Task.async(function*() {
info ("$_ returns undefined if nothing has evaluated yet");
let response = yield evaluateJS("$_");
basicResultCheck(response, "$_", undefined);
nextTest();
});
let checkAdditionResult = Task.async(function*() {
info ("$_ returns last value and performs basic arithmetic");
let response = yield evaluateJS("2+2");
basicResultCheck(response, "2+2", 4);
response = yield evaluateJS("$_");
basicResultCheck(response, "$_", 4);
response = yield evaluateJS("$_ + 2");
basicResultCheck(response, "$_ + 2", 6);
response = yield evaluateJS("$_ + 4");
basicResultCheck(response, "$_ + 4", 10);
nextTest();
});
let checkObjectResult = Task.async(function*() {
info ("$_ has correct references to objects");
let response = yield evaluateJS("var foo = {bar:1}; foo;");
basicResultCheck(response, "var foo = {bar:1}; foo;", {
type: "object",
class: "Object",
actor: /[a-z]/,
});
checkObject(response.result.preview.ownProperties, {
bar: {
value: 1
}
});
response = yield evaluateJS("$_");
basicResultCheck(response, "$_", {
type: "object",
class: "Object",
actor: /[a-z]/,
});
checkObject(response.result.preview.ownProperties, {
bar: {
value: 1
}
});
top.foo.bar = 2;
response = yield evaluateJS("$_");
basicResultCheck(response, "$_", {
type: "object",
class: "Object",
actor: /[a-z]/,
});
checkObject(response.result.preview.ownProperties, {
bar: {
value: 2
}
});
nextTest();
});
function basicResultCheck(response, input, output) {
checkObject(response, {
from: gState.actor,
input: input,
result: output,
});
ok(!response.exception, "no eval exception");
ok(!response.helperResult, "no helper result");
}
function testEnd()
{
closeDebugger(gState, function() {
gState = null;
SimpleTest.finish();
});
}
addEventListener("load", startTest);
</script>
</body>
</html>

View File

@ -1549,6 +1549,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.
*