Bug 1366646 - Include borders and padding when calculating the offset of a window inside an (i)frame. r=jaws

MozReview-Commit-ID: 58fBRcw1lg3

--HG--
extra : rebase_source : 7e125ba0203ce56e9782152b036ab5c26e0a3aa5
This commit is contained in:
Mike de Boer 2017-10-18 13:58:36 +02:00
Родитель 3930673f33
Коммит 8678d63172
7 изменённых файлов: 324 добавлений и 201 удалений

Просмотреть файл

@ -5,7 +5,7 @@
function frameScript() {
function getSelectedText() {
let frame = this.content.frames[0].frames[1];
let Ci = Components.interfaces;
let {interfaces: Ci, utils: Cu} = Components;
let docShell = frame.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
@ -14,10 +14,18 @@ function frameScript() {
.QueryInterface(Ci.nsISelectionController);
let selection = controller.getSelection(controller.SELECTION_FIND);
let range = selection.getRangeAt(0);
let scope = {};
Cu.import("resource://gre/modules/FindContent.jsm", scope);
let highlighter = (new scope.FindContent(docShell)).highlighter;
let r1 = frame.parent.frameElement.getBoundingClientRect();
let f1 = highlighter._getFrameElementOffsets(frame.parent);
let r2 = frame.frameElement.getBoundingClientRect();
let f2 = highlighter._getFrameElementOffsets(frame);
let r3 = range.getBoundingClientRect();
let rect = {top: (r1.top + r2.top + r3.top), left: (r1.left + r2.left + r3.left)};
let rect = {
top: (r1.top + r2.top + r3.top + f1.y + f2.y),
left: (r1.left + r2.left + r3.left + f1.x + f2.x),
};
this.sendAsyncMessage("test:find:selectionTest", {text: selection.toString(), rect});
}
getSelectedText();

Просмотреть файл

@ -292,11 +292,12 @@ FinderHighlighter.prototype = {
} else {
let findSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
findSelection.addRange(range);
// Check if the range is inside an iframe.
// Check if the range is inside an (i)frame.
if (window != window.top) {
let dict = this.getForWindow(window.top);
if (!dict.frames.has(window))
dict.frames.set(window, null);
// Add this frame to the list, so that we'll be able to find it later
// when we need to clear its selection(s).
dict.frames.set(window, {});
}
}
@ -572,6 +573,9 @@ FinderHighlighter.prototype = {
// If we're in a frame, update the position of the rect (top/ left).
let currWin = window;
while (currWin != window.top) {
let frameOffsets = this._getFrameElementOffsets(currWin);
cssPageRect.translate(frameOffsets.x, frameOffsets.y);
// Since the frame is an element inside a parent window, we'd like to
// learn its position relative to it.
let el = this._getDWU(currWin).containerElement;
@ -594,10 +598,48 @@ FinderHighlighter.prototype = {
cssPageRect.translate(parentRect.left, parentRect.top);
}
let frameOffsets = this._getFrameElementOffsets(currWin);
cssPageRect.translate(frameOffsets.x, frameOffsets.y);
return cssPageRect;
},
/**
* (I)Frame elements may have a border and/ or padding set, which is not
* included in the bounds returned by nsDOMWindowUtils#getRootBounds() for the
* window it hosts.
* This method fetches this offset of the frame element to the respective window.
*
* @param {nsIDOMWindow} window Window to read the boundary rect from
* @return {Object} Simple object that contains the following two properties:
* - {Number} x Offset along the horizontal axis.
* - {Number} y Offset along the vertical axis.
*/
_getFrameElementOffsets(window) {
let frame = window.frameElement;
if (!frame)
return { x: 0, y: 0 };
// Getting style info is super expensive, causing reflows, so let's cache
// frame border widths and padding values aggressively.
let dict = this.getForWindow(window.top);
let frameData = dict.frames.get(window);
if (!frameData)
dict.frames.set(window, frameData = {});
if (frameData.offset)
return frameData.offset;
let style = frame.ownerGlobal.getComputedStyle(frame);
// We only need to left sides, because ranges are offset from point 0,0 in
// the top-left corner.
let borderOffset = [parseInt(style.borderLeftWidth, 10) || 0, parseInt(style.borderTopWidth, 10) || 0];
let paddingOffset = [parseInt(style.paddingLeft, 10) || 0, parseInt(style.paddingTop, 10) || 0];
return frameData.offset = {
x: borderOffset[0] + paddingOffset[0],
y: borderOffset[1] + paddingOffset[1]
};
},
/**
* Utility; fetch the full width and height of the current window, excluding
* scrollbars.
@ -761,7 +803,7 @@ FinderHighlighter.prototype = {
// Check if we're in a frameset (including iframes).
if (window != window.top) {
if (!dict.frames.has(window))
dict.frames.set(window, null);
dict.frames.set(window, {});
return true;
}
@ -791,13 +833,13 @@ FinderHighlighter.prototype = {
let bounds;
// If the window is part of a frameset, try to cache the bounds query.
if (dict && dict.frames.has(window)) {
bounds = dict.frames.get(window);
if (!bounds) {
bounds = this._getRootBounds(window);
dict.frames.set(window, bounds);
}
} else
let frameData = dict.frames.get(window);
bounds = frameData.bounds;
if (!bounds)
bounds = frameData.bounds = this._getRootBounds(window);
} else {
bounds = this._getRootBounds(window);
}
let topBounds = this._getRootBounds(window.top, false);
let rects = [];
@ -856,8 +898,8 @@ FinderHighlighter.prototype = {
*/
_updateDynamicRangesRects(dict) {
// Reset the frame bounds cache.
for (let frame of dict.frames.keys())
dict.frames.set(frame, null);
for (let frameData of dict.frames.values())
frameData.bounds = null;
for (let range of dict.dynamicRangesSet)
this._updateRangeRects(range, false, dict);
},

Просмотреть файл

@ -4,6 +4,8 @@ support-files =
metadata_*.html
testremotepagemanager.html
testremotepagemanager2.html
file_FinderIframeTest.html
file_FinderSample.html
file_WebNavigation_page1.html
file_WebNavigation_page2.html
file_WebNavigation_page3.html
@ -19,6 +21,7 @@ support-files =
file_script_bad.js
file_script_redirect.js
file_script_xhr.js
head.js
WebRequest_dynamic.sjs
WebRequest_redirection.sjs
@ -30,7 +33,8 @@ support-files =
[browser_Finder_offscreen_text.js]
[browser_FinderHighlighter.js]
skip-if = debug || os = "linux"
support-files = file_FinderSample.html
[browser_FinderHighlighter2.js]
skip-if = debug || os = "linux"
[browser_Geometry.js]
[browser_InlineSpellChecker.js]
[browser_WebNavigation.js]

Просмотреть файл

@ -1,193 +1,13 @@
/* eslint-disable mozilla/no-arbitrary-setTimeout */
"use strict";
Cu.import("resource://testing-common/BrowserTestUtils.jsm", this);
Cu.import("resource://testing-common/ContentTask.jsm", this);
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/Timer.jsm", this);
Cu.import("resource://gre/modules/AppConstants.jsm");
const kHighlightAllPref = "findbar.highlightAll";
const kPrefModalHighlight = "findbar.modalHighlight";
const kFixtureBaseURL = "https://example.com/browser/toolkit/modules/tests/browser/";
const kIteratorTimeout = Services.prefs.getIntPref("findbar.iteratorTimeout");
function promiseOpenFindbar(findbar) {
findbar.onFindCommand();
return gFindBar._startFindDeferred && gFindBar._startFindDeferred.promise;
}
function promiseFindResult(findbar, str = null) {
let highlightFinished = false;
let findFinished = false;
return new Promise(resolve => {
let listener = {
onFindResult({ searchString }) {
if (str !== null && str != searchString) {
return;
}
findFinished = true;
if (highlightFinished) {
findbar.browser.finder.removeResultListener(listener);
resolve();
}
},
onHighlightFinished() {
highlightFinished = true;
if (findFinished) {
findbar.browser.finder.removeResultListener(listener);
resolve();
}
},
onMatchesCountResult: () => {}
};
findbar.browser.finder.addResultListener(listener);
});
}
function promiseEnterStringIntoFindField(findbar, str) {
let promise = promiseFindResult(findbar, str);
for (let i = 0; i < str.length; i++) {
let event = document.createEvent("KeyboardEvent");
event.initKeyEvent("keypress", true, true, null, false, false,
false, false, 0, str.charCodeAt(i));
findbar._findField.inputField.dispatchEvent(event);
}
return promise;
}
function promiseTestHighlighterOutput(browser, word, expectedResult, extraTest = () => {}) {
return ContentTask.spawn(browser, { word, expectedResult, extraTest: extraTest.toSource() },
async function({ word, expectedResult, extraTest }) {
Cu.import("resource://gre/modules/Timer.jsm", this);
return new Promise((resolve, reject) => {
let stubbed = {};
let callCounts = {
insertCalls: [],
removeCalls: [],
animationCalls: []
};
let lastMaskNode, lastOutlineNode;
let rects = [];
// Amount of milliseconds to wait after the last time one of our stubs
// was called.
const kTimeoutMs = 1000;
// The initial timeout may wait for a while for results to come in.
let timeout = setTimeout(() => finish(false, "Timeout"), kTimeoutMs * 5);
function finish(ok = true, message = "finished with error") {
// Restore the functions we stubbed out.
try {
content.document.insertAnonymousContent = stubbed.insert;
content.document.removeAnonymousContent = stubbed.remove;
} catch (ex) {}
stubbed = {};
clearTimeout(timeout);
if (expectedResult.rectCount !== 0)
Assert.ok(ok, message);
Assert.greaterOrEqual(callCounts.insertCalls.length, expectedResult.insertCalls[0],
`Min. insert calls should match for '${word}'.`);
Assert.lessOrEqual(callCounts.insertCalls.length, expectedResult.insertCalls[1],
`Max. insert calls should match for '${word}'.`);
Assert.greaterOrEqual(callCounts.removeCalls.length, expectedResult.removeCalls[0],
`Min. remove calls should match for '${word}'.`);
Assert.lessOrEqual(callCounts.removeCalls.length, expectedResult.removeCalls[1],
`Max. remove calls should match for '${word}'.`);
// We reached the amount of calls we expected, so now we can check
// the amount of rects.
if (!lastMaskNode && expectedResult.rectCount !== 0) {
Assert.ok(false, `No mask node found, but expected ${expectedResult.rectCount} rects.`);
}
Assert.equal(rects.length, expectedResult.rectCount,
`Amount of inserted rects should match for '${word}'.`);
if ("animationCalls" in expectedResult) {
Assert.greaterOrEqual(callCounts.animationCalls.length,
expectedResult.animationCalls[0], `Min. animation calls should match for '${word}'.`);
Assert.lessOrEqual(callCounts.animationCalls.length,
expectedResult.animationCalls[1], `Max. animation calls should match for '${word}'.`);
}
// Allow more specific assertions to be tested in `extraTest`.
// eslint-disable-next-line no-eval
extraTest = eval(extraTest);
extraTest(lastMaskNode, lastOutlineNode, rects);
resolve();
}
function stubAnonymousContentNode(domNode, anonNode) {
let originals = [anonNode.setTextContentForElement,
anonNode.setAttributeForElement, anonNode.removeAttributeForElement,
anonNode.setCutoutRectsForElement, anonNode.setAnimationForElement];
anonNode.setTextContentForElement = (id, text) => {
try {
(domNode.querySelector("#" + id) || domNode).textContent = text;
} catch (ex) {}
return originals[0].call(anonNode, id, text);
};
anonNode.setAttributeForElement = (id, attrName, attrValue) => {
try {
(domNode.querySelector("#" + id) || domNode).setAttribute(attrName, attrValue);
} catch (ex) {}
return originals[1].call(anonNode, id, attrName, attrValue);
};
anonNode.removeAttributeForElement = (id, attrName) => {
try {
let node = domNode.querySelector("#" + id) || domNode;
if (node.hasAttribute(attrName))
node.removeAttribute(attrName);
} catch (ex) {}
return originals[2].call(anonNode, id, attrName);
};
anonNode.setCutoutRectsForElement = (id, cutoutRects) => {
rects = cutoutRects;
return originals[3].call(anonNode, id, cutoutRects);
};
anonNode.setAnimationForElement = (id, keyframes, options) => {
callCounts.animationCalls.push([keyframes, options]);
return originals[4].call(anonNode, id, keyframes, options);
};
}
// Create a function that will stub the original version and collects
// the arguments so we can check the results later.
function stub(which) {
stubbed[which] = content.document[which + "AnonymousContent"];
let prop = which + "Calls";
return function(node) {
callCounts[prop].push(node);
if (which == "insert") {
if (node.outerHTML.indexOf("outlineMask") > -1)
lastMaskNode = node;
else
lastOutlineNode = node;
}
clearTimeout(timeout);
timeout = setTimeout(() => {
finish();
}, kTimeoutMs);
let res = stubbed[which].call(content.document, node);
if (which == "insert")
stubAnonymousContentNode(node, res);
return res;
};
}
content.document.insertAnonymousContent = stub("insert");
content.document.removeAnonymousContent = stub("remove");
});
});
}
const kPrefHighlightAll = "findbar.highlightAll";
const kPrefModalHighlight = "findbar.modalHighlight";
add_task(async function setup() {
await SpecialPowers.pushPrefEnv({ set: [
[kHighlightAllPref, true],
[kPrefHighlightAll, true],
[kPrefModalHighlight, true]
]});
});
@ -205,7 +25,8 @@ add_task(async function testModalResults() {
rectCount: 2,
insertCalls: [5, 6],
removeCalls: [4, 5],
extraTest(maskNode, outlineNode, rects) {
// eslint-disable-next-line object-shorthand
extraTest: function(maskNode, outlineNode, rects) {
Assert.equal(outlineNode.getElementsByTagName("div").length, 2,
"There should be multiple rects drawn");
}
@ -373,7 +194,7 @@ add_task(async function testHighlightAllToggle() {
removeCalls: [1, 2]
};
promise = promiseTestHighlighterOutput(browser, word, expectedResult);
await SpecialPowers.pushPrefEnv({ "set": [[ kHighlightAllPref, false ]] });
await SpecialPowers.pushPrefEnv({ "set": [[ kPrefHighlightAll, false ]] });
await promise;
// For posterity, let's switch back.
@ -383,7 +204,7 @@ add_task(async function testHighlightAllToggle() {
removeCalls: [0, 1]
};
promise = promiseTestHighlighterOutput(browser, word, expectedResult);
await SpecialPowers.pushPrefEnv({ "set": [[ kHighlightAllPref, true ]] });
await SpecialPowers.pushPrefEnv({ "set": [[ kPrefHighlightAll, true ]] });
await promise;
});
});

Просмотреть файл

@ -0,0 +1,51 @@
"use strict";
const kPrefHighlightAll = "findbar.highlightAll";
const kPrefModalHighlight = "findbar.modalHighlight";
add_task(async function setup() {
await SpecialPowers.pushPrefEnv({ set: [
[kPrefHighlightAll, true],
[kPrefModalHighlight, true]
]});
});
add_task(async function testIframeOffset() {
let url = kFixtureBaseURL + "file_FinderIframeTest.html";
await BrowserTestUtils.withNewTab(url, async function(browser) {
let findbar = gBrowser.getFindBar();
await promiseOpenFindbar(findbar);
let word = "frame";
let expectedResult = {
rectCount: 12,
insertCalls: [2, 4],
removeCalls: [0, 2]
};
let promise = promiseTestHighlighterOutput(browser, word, expectedResult, (maskNode, outlineNode, rects) => {
Assert.equal(rects.length, expectedResult.rectCount, "Rect counts should match");
// Checks to guard against regressing this functionality:
let expectedOffsets = [
{ x: 16, y: 60 },
{ x: 68, y: 104 },
{ x: 21, y: 215 },
{ x: 78, y: 264 },
{ x: 21, y: 375 },
{ x: 78, y: 424 },
{ x: 20, y: 534 },
{ x: 93, y: 534 },
{ x: 71, y: 577 },
{ x: 145, y: 577 }
]
for (let i = 1, l = rects.length - 1; i < l; ++i) {
let rect = rects[i];
let expected = expectedOffsets[i - 1];
Assert.equal(Math.floor(rect.x), expected.x, "Horizontal offset should match for rect " + i);
Assert.equal(Math.floor(rect.y), expected.y, "Vertical offset should match for rect " + i);
}
});
await promiseEnterStringIntoFindField(findbar, word);
await promise;
});
});

Просмотреть файл

@ -0,0 +1,21 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Test (i)frame offsets, bug 1366646</title>
</head>
<body>
<p>top level frame</p>
<iframe src="data:text/html,&lt;p&gt;frame without border&lt;/p&gt;
&lt;iframe src='data:text/html,&lt;p&gt;nested frame without border&lt;/p&gt;' height='50' width='100%' style='background-color: yellow; border: 0'&gt;&lt;/iframe&gt;
" style="background-color: pink; border: 0" width="100%" height="150"></iframe>
<iframe src="data:text/html,&lt;p&gt;frame with 5px border&lt;/p&gt;
&lt;iframe src='data:text/html,&lt;p&gt;nested frame with 5px border&lt;/p&gt;' height='50' width='100%' style='background-color: yellow; border: solid 5px black'&gt;&lt;/iframe&gt;
" style="background-color: pink; border: solid 5px black" width="100%" height="150"></iframe>
<iframe src="data:text/html,&lt;p&gt;frame with 5px padding&lt;/p&gt;
&lt;iframe src='data:text/html,&lt;p&gt;nested frame with 5px padding&lt;/p&gt;' height='50' width='100%' style='background-color: yellow; border: 0; padding: 5px'&gt;&lt;/iframe&gt;
" style="background-color: pink; border: 0; padding: 5px" width="100%" height="150"></iframe>
<!-- Testing deprecated HTML4 iframe properties too: -->
<iframe src="data:text/html,&lt;p&gt;frame with frameborder, marginwidth/ height and 5px padding&lt;/p&gt;
&lt;iframe src='data:text/html,&lt;p&gt;nested frame with frameborder, marginwidth/ height&lt;/p&gt;' height='50' width='100%' frameborder='1' marginheight='5' marginwidth='5' style='background-color: yellow;'&gt;&lt;/iframe&gt;
" frameborder="1" marginheight="5" marginwidth="5" style="background-color: pink; padding: 5px" width="100%" height="150"></iframe>
</body></html>

Просмотреть файл

@ -1,3 +1,9 @@
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout", "resource://gre/modules/Timer.jsm");
const kFixtureBaseURL = "https://example.com/browser/toolkit/modules/tests/browser/";
function removeDupes(list) {
let j = 0;
for (let i = 1; i < list.length; i++) {
@ -19,3 +25,173 @@ function compareLists(list1, list2, kind) {
is(String(list1), String(list2), `${kind} URLs correct`);
}
function promiseOpenFindbar(findbar) {
findbar.onFindCommand();
return gFindBar._startFindDeferred && gFindBar._startFindDeferred.promise;
}
function promiseFindResult(findbar, str = null) {
let highlightFinished = false;
let findFinished = false;
return new Promise(resolve => {
let listener = {
onFindResult({ searchString }) {
if (str !== null && str != searchString) {
return;
}
findFinished = true;
if (highlightFinished) {
findbar.browser.finder.removeResultListener(listener);
resolve();
}
},
onHighlightFinished() {
highlightFinished = true;
if (findFinished) {
findbar.browser.finder.removeResultListener(listener);
resolve();
}
},
onMatchesCountResult: () => {}
};
findbar.browser.finder.addResultListener(listener);
});
}
function promiseEnterStringIntoFindField(findbar, str) {
let promise = promiseFindResult(findbar, str);
for (let i = 0; i < str.length; i++) {
let event = document.createEvent("KeyboardEvent");
event.initKeyEvent("keypress", true, true, null, false, false,
false, false, 0, str.charCodeAt(i));
findbar._findField.inputField.dispatchEvent(event);
}
return promise;
}
function promiseTestHighlighterOutput(browser, word, expectedResult, extraTest = () => {}) {
return ContentTask.spawn(browser, { word, expectedResult, extraTest: extraTest.toSource() },
async function({ word, expectedResult, extraTest }) {
return new Promise((resolve, reject) => {
let stubbed = {};
let callCounts = {
insertCalls: [],
removeCalls: [],
animationCalls: []
};
let lastMaskNode, lastOutlineNode;
let rects = [];
// Amount of milliseconds to wait after the last time one of our stubs
// was called.
const kTimeoutMs = 1000;
// The initial timeout may wait for a while for results to come in.
let timeout = setTimeout(() => finish(false, "Timeout"), kTimeoutMs * 5);
function finish(ok = true, message = "finished with error") {
// Restore the functions we stubbed out.
try {
content.document.insertAnonymousContent = stubbed.insert;
content.document.removeAnonymousContent = stubbed.remove;
} catch (ex) {}
stubbed = {};
clearTimeout(timeout);
if (expectedResult.rectCount !== 0)
Assert.ok(ok, message);
Assert.greaterOrEqual(callCounts.insertCalls.length, expectedResult.insertCalls[0],
`Min. insert calls should match for '${word}'.`);
Assert.lessOrEqual(callCounts.insertCalls.length, expectedResult.insertCalls[1],
`Max. insert calls should match for '${word}'.`);
Assert.greaterOrEqual(callCounts.removeCalls.length, expectedResult.removeCalls[0],
`Min. remove calls should match for '${word}'.`);
Assert.lessOrEqual(callCounts.removeCalls.length, expectedResult.removeCalls[1],
`Max. remove calls should match for '${word}'.`);
// We reached the amount of calls we expected, so now we can check
// the amount of rects.
if (!lastMaskNode && expectedResult.rectCount !== 0) {
Assert.ok(false, `No mask node found, but expected ${expectedResult.rectCount} rects.`);
}
Assert.equal(rects.length, expectedResult.rectCount,
`Amount of inserted rects should match for '${word}'.`);
if ("animationCalls" in expectedResult) {
Assert.greaterOrEqual(callCounts.animationCalls.length,
expectedResult.animationCalls[0], `Min. animation calls should match for '${word}'.`);
Assert.lessOrEqual(callCounts.animationCalls.length,
expectedResult.animationCalls[1], `Max. animation calls should match for '${word}'.`);
}
// Allow more specific assertions to be tested in `extraTest`.
// eslint-disable-next-line no-eval
extraTest = eval(extraTest);
extraTest(lastMaskNode, lastOutlineNode, rects);
resolve();
}
function stubAnonymousContentNode(domNode, anonNode) {
let originals = [anonNode.setTextContentForElement,
anonNode.setAttributeForElement, anonNode.removeAttributeForElement,
anonNode.setCutoutRectsForElement, anonNode.setAnimationForElement];
anonNode.setTextContentForElement = (id, text) => {
try {
(domNode.querySelector("#" + id) || domNode).textContent = text;
} catch (ex) {}
return originals[0].call(anonNode, id, text);
};
anonNode.setAttributeForElement = (id, attrName, attrValue) => {
try {
(domNode.querySelector("#" + id) || domNode).setAttribute(attrName, attrValue);
} catch (ex) {}
return originals[1].call(anonNode, id, attrName, attrValue);
};
anonNode.removeAttributeForElement = (id, attrName) => {
try {
let node = domNode.querySelector("#" + id) || domNode;
if (node.hasAttribute(attrName))
node.removeAttribute(attrName);
} catch (ex) {}
return originals[2].call(anonNode, id, attrName);
};
anonNode.setCutoutRectsForElement = (id, cutoutRects) => {
rects = cutoutRects;
return originals[3].call(anonNode, id, cutoutRects);
};
anonNode.setAnimationForElement = (id, keyframes, options) => {
callCounts.animationCalls.push([keyframes, options]);
return originals[4].call(anonNode, id, keyframes, options);
};
}
// Create a function that will stub the original version and collects
// the arguments so we can check the results later.
function stub(which) {
stubbed[which] = content.document[which + "AnonymousContent"];
let prop = which + "Calls";
return function(node) {
callCounts[prop].push(node);
if (which == "insert") {
if (node.outerHTML.indexOf("outlineMask") > -1)
lastMaskNode = node;
else
lastOutlineNode = node;
}
clearTimeout(timeout);
timeout = setTimeout(() => {
finish();
}, kTimeoutMs);
let res = stubbed[which].call(content.document, node);
if (which == "insert")
stubAnonymousContentNode(node, res);
return res;
};
}
content.document.insertAnonymousContent = stub("insert");
content.document.removeAnonymousContent = stub("remove");
});
});
}