Bug 1866437 - Add some telemetry for the highlight feature in pdf.js r=pdfjs-reviewers,marco

Differential Revision: https://phabricator.services.mozilla.com/D203023
This commit is contained in:
Calixte 2024-02-29 12:42:29 +00:00
parent 23b0c44284
commit 6e5d2981a4
8 changed files with 630 additions and 88 deletions

View file

@ -265,6 +265,8 @@ export class GeckoViewPdfjsParent extends GeckoViewActorParent {
return this.#getExperimentFeature();
case "PDFJS:Parent:recordExposure":
return this.#recordExposure();
case "PDFJS:Parent:reportTelemetry":
return this.#reportTelemetry(aMsg);
default:
break;
}
@ -355,6 +357,10 @@ export class GeckoViewPdfjsParent extends GeckoViewActorParent {
warn`Cannot record experiment exposure: ${e}`;
}
}
#reportTelemetry(aMsg) {
lazy.PdfJsTelemetry.report(aMsg.data);
}
}
const { debug, warn } = GeckoViewPdfjsParent.initLogging(

View file

@ -12,44 +12,127 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint max-len: ["error", 100] */
export const PdfJsTelemetry = {
onViewerIsUsed() {
export class PdfJsTelemetryContent {
static onViewerIsUsed() {
Glean.pdfjs.used.add(1);
},
onTimeToView(ms) {
}
}
export class PdfJsTelemetry {
static report(aData) {
const { type } = aData;
switch (type) {
case "pageInfo":
this.onTimeToView(aData.timestamp);
break;
case "editing":
this.onEditing(aData);
break;
case "buttons":
case "gv-buttons":
{
const id = aData.data.id.replace(
/([A-Z])/g,
c => `_${c.toLowerCase()}`
);
if (type === "buttons") {
this.onButtons(id);
} else {
this.onGeckoview(id);
}
}
break;
}
}
static onTimeToView(ms) {
Glean.pdfjs.timeToView.accumulateSamples([ms]);
},
onEditing({ subtype, data }) {
if (!data) {
return;
}
if (!subtype && data.type) {
Glean.pdfjs.editing[data.type].add(1);
}
static onEditing({ type, data }) {
if (type !== "editing" || !data) {
return;
}
if (subtype !== "stamp") {
return;
switch (data.type) {
case "freetext":
case "ink":
Glean.pdfjs.editing[data.type].add(1);
return;
case "print":
case "save":
{
Glean.pdfjs.editing[data.type].add(1);
if (!data.stats) {
return;
}
const numbers = ["one", "two", "three", "four", "five"];
Glean.pdfjsEditingHighlight[data.type].add(1);
Glean.pdfjsEditingHighlight.numberOfColors[
numbers[data.stats.highlight.numberOfColors - 1]
].add(1);
}
return;
case "stamp":
if (data.action === "added") {
Glean.pdfjs.editing.stamp.add(1);
return;
}
Glean.pdfjs.stamp[data.action].add(1);
for (const key of [
"alt_text_keyboard",
"alt_text_decorative",
"alt_text_description",
"alt_text_edit",
]) {
if (data[key]) {
Glean.pdfjs.stamp[key].add(1);
}
}
return;
case "highlight":
case "free_highlight":
switch (data.action) {
case "added":
Glean.pdfjsEditingHighlight.kind[data.type].add(1);
Glean.pdfjsEditingHighlight.method[data.methodOfCreation].add(1);
Glean.pdfjsEditingHighlight.color[data.color].add(1);
if (data.type === "free_highlight") {
Glean.pdfjsEditingHighlight.thickness.accumulateSamples([
data.thickness,
]);
}
break;
case "color_changed":
Glean.pdfjsEditingHighlight.color[data.color].add(1);
Glean.pdfjsEditingHighlight.colorChanged.add(1);
break;
case "thickness_changed":
Glean.pdfjsEditingHighlight.thickness.accumulateSamples([
data.thickness,
]);
Glean.pdfjsEditingHighlight.thicknessChanged.add(1);
break;
case "deleted":
Glean.pdfjsEditingHighlight.deleted.add(1);
break;
case "edited":
Glean.pdfjsEditingHighlight.edited.add(1);
break;
case "toggle_visibility":
Glean.pdfjsEditingHighlight.toggleVisibility.add(1);
break;
}
break;
}
}
Glean.pdfjs.stamp[data.action].add(1);
for (const key of [
"alt_text_keyboard",
"alt_text_decorative",
"alt_text_description",
"alt_text_edit",
]) {
if (data[key]) {
Glean.pdfjs.stamp[key].add(1);
}
}
},
onButtons(id) {
static onButtons(id) {
Glean.pdfjs.buttons[id].add(1);
},
onGeckoview(id) {
}
static onGeckoview(id) {
Glean.pdfjs.geckoview[id].add(1);
},
};
}
}

View file

@ -27,7 +27,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
NetworkManager: "resource://pdf.js/PdfJsNetwork.sys.mjs",
PdfJs: "resource://pdf.js/PdfJs.sys.mjs",
PdfJsTelemetry: "resource://pdf.js/PdfJsTelemetry.sys.mjs",
PdfJsTelemetryContent: "resource://pdf.js/PdfJsTelemetry.sys.mjs",
PdfSandbox: "resource://pdf.js/PdfSandbox.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});
@ -331,27 +331,8 @@ class ChromeActions {
reportTelemetry(data) {
const probeInfo = JSON.parse(data);
const { type } = probeInfo;
switch (type) {
case "pageInfo":
lazy.PdfJsTelemetry.onTimeToView(probeInfo.timestamp);
break;
case "editing":
lazy.PdfJsTelemetry.onEditing(probeInfo);
break;
case "buttons":
case "gv-buttons":
const id = probeInfo.data.id.replace(
/([A-Z])/g,
c => `_${c.toLowerCase()}`
);
if (type === "buttons") {
lazy.PdfJsTelemetry.onButtons(id);
} else {
lazy.PdfJsTelemetry.onGeckoview(id);
}
break;
}
const actor = getActor(this.domWindow);
actor?.sendAsyncMessage("PDFJS:Parent:reportTelemetry", probeInfo);
}
updateFindControlState(data) {
@ -1010,7 +991,7 @@ PdfStreamConverter.prototype = {
aRequest.setResponseHeader("Refresh", "", false);
}
lazy.PdfJsTelemetry.onViewerIsUsed();
lazy.PdfJsTelemetryContent.onViewerIsUsed();
// The document will be loaded via the stream converter as html,
// but since we may have come here via a download or attachment

View file

@ -19,6 +19,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
PdfJsTelemetry: "resource://pdf.js/PdfJsTelemetry.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
SetClipboardSearchString: "resource://gre/modules/Finder.sys.mjs",
});
@ -69,6 +70,8 @@ export class PdfjsParent extends JSWindowActorParent {
return this._saveURL(aMsg);
case "PDFJS:Parent:recordExposure":
return this._recordExposure();
case "PDFJS:Parent:reportTelemetry":
return this._reportTelemetry(aMsg);
}
return undefined;
}
@ -85,6 +88,10 @@ export class PdfjsParent extends JSWindowActorParent {
lazy.NimbusFeatures.pdfjs.recordExposureEvent({ once: true });
}
_reportTelemetry(aMsg) {
lazy.PdfJsTelemetry.report(aMsg.data);
}
_saveURL(aMsg) {
const data = aMsg.data;
this.browser.ownerGlobal.saveURL(

View file

@ -140,3 +140,195 @@ pdfjs:
notification_emails:
- cdenizet@mozilla.com
- mcastelluccio@mozilla.com
pdfjs.editing.highlight:
kind:
type: labeled_counter
labels:
- free_highlight
- highlight
description: >
Counts the number of times a given kind is used to highlight.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437#c1
data_sensitivity:
- interaction
notification_emails:
- pdfjs-team@mozilla.com
expires: never
method:
type: labeled_counter
labels:
- context_menu
- main_toolbar
- floating_button
description: >
Counts the number of times a given method is used to highlight.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437#c1
data_sensitivity:
- interaction
notification_emails:
- pdfjs-team@mozilla.com
expires: never
color:
type: labeled_counter
labels:
- yellow
- green
- blue
- pink
- red
description: >
Counts the number of times a given color is used to highlight.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437#c1
data_sensitivity:
- interaction
notification_emails:
- pdfjs-team@mozilla.com
expires: never
color_changed:
type: counter
description: >
Counts the number of times the user changes the color of a highlight.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437#c1
data_sensitivity:
- interaction
notification_emails:
- pdfjs-team@mozilla.com
expires: never
number_of_colors:
type: labeled_counter
labels:
- one
- two
- three
- four
- five
description: >
Counts the number of different colors used to highlight.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437#c1
data_sensitivity:
- interaction
notification_emails:
- pdfjs-team@mozilla.com
expires: never
thickness:
type: custom_distribution
range_min: 8
range_max: 24
bucket_count: 17
histogram_type: linear
unit: pixels
description: >
The thickness used to draw a free highlight.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437#c1
data_sensitivity:
- interaction
notification_emails:
- pdfjs-team@mozilla.com
expires: never
thickness_changed:
type: counter
description: >
Counts the number of times the user changes the thickness of a free highlight.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437#c1
data_sensitivity:
- interaction
notification_emails:
- pdfjs-team@mozilla.com
expires: never
save:
type: counter
description: >
Counts the number of times the user saves a PDF with highlights.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437#c1
data_sensitivity:
- interaction
notification_emails:
- pdfjs-team@mozilla.com
expires: never
print:
type: counter
description: >
Counts the number of times the user prints a PDF with highlights.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437#c1
data_sensitivity:
- interaction
notification_emails:
- pdfjs-team@mozilla.com
expires: never
edited:
type: counter
description: >
Counts the number of times the user edits highlights.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437#c1
data_sensitivity:
- interaction
notification_emails:
- pdfjs-team@mozilla.com
expires: never
deleted:
type: counter
description: >
Counts the number of times the user deletes highlights.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437#c1
data_sensitivity:
- interaction
notification_emails:
- pdfjs-team@mozilla.com
expires: never
toggle_visibility:
type: counter
description: >
Counts the number of times the user toggles the visibility of highlights.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1866437#c1
data_sensitivity:
- interaction
notification_emails:
- pdfjs-team@mozilla.com
expires: never

View file

@ -33,6 +33,9 @@ support-files = ["file_pdfjs_form.pdf"]
["browser_pdfjs_hcm.js"]
support-files = ["file_pdfjs_hcm.pdf"]
["browser_pdfjs_highlight_telemetry.js"]
skip-if = ["true"]
["browser_pdfjs_js.js"]
support-files = ["file_pdfjs_js.pdf"]

View file

@ -0,0 +1,234 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const RELATIVE_DIR = "toolkit/components/pdfjs/test/";
const TESTROOT = "https://example.com/browser/" + RELATIVE_DIR;
// Test telemetry.
add_task(async function test() {
let mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
let handlerInfo = mimeService.getFromTypeAndExtension(
"application/pdf",
"pdf"
);
// Make sure pdf.js is the default handler.
is(
handlerInfo.alwaysAskBeforeHandling,
false,
"pdf handler defaults to always-ask is false"
);
is(
handlerInfo.preferredAction,
Ci.nsIHandlerInfo.handleInternally,
"pdf handler defaults to internal"
);
info("Pref action: " + handlerInfo.preferredAction);
await BrowserTestUtils.withNewTab(
{ gBrowser, url: "about:blank" },
async function (browser) {
await SpecialPowers.pushPrefEnv({
set: [
["pdfjs.annotationEditorMode", 0],
["pdfjs.enableHighlight", true],
],
});
Services.fog.testResetFOG();
// check that PDF is opened with internal viewer
await waitForPdfJSAllLayers(browser, TESTROOT + "file_pdfjs_test.pdf", [
[
"annotationEditorLayer",
"annotationLayer",
"textLayer",
"canvasWrapper",
],
["annotationEditorLayer", "textLayer", "canvasWrapper"],
]);
await Services.fog.testFlushAllChildren();
Assert.equal(
(Glean.pdfjsEditingHighlight.kind.freeHighlight.testGetValue() || 0) +
(Glean.pdfjsEditingHighlight.kind.highlight.testGetValue() || 0),
0,
"Should have no highlight"
);
await enableEditor(browser, "Highlight");
const strs = ["In production", "buildbot automation"];
for (let i = 0; i < strs.length; i++) {
const str = strs[i];
const N = i + 1;
const spanBox = await getSpanBox(browser, str);
await clickAt(
browser,
spanBox.x + 0.75 * spanBox.width,
spanBox.y + 0.5 * spanBox.height,
2
);
await waitForEditors(browser, ".highlightEditor", N);
await Services.fog.testFlushAllChildren();
Assert.equal(
Glean.pdfjsEditingHighlight.kind.highlight.testGetValue(),
N,
`Should have ${N} highlights`
);
Assert.equal(
Glean.pdfjsEditingHighlight.color.yellow.testGetValue(),
N,
`Should have ${N} yellow highlights`
);
Assert.equal(
Glean.pdfjsEditingHighlight.method.main_toolbar.testGetValue(),
N,
`Should have ${N} highlights created from the toolbar`
);
await EventUtils.synthesizeKey("KEY_Escape");
await waitForSelector(browser, ".highlightEditor:not(.selectedEditor)");
document.getElementById("cmd_print").doCommand();
await BrowserTestUtils.waitForCondition(() => {
let preview = document.querySelector(".printPreviewBrowser");
return preview && BrowserTestUtils.isVisible(preview);
});
await EventUtils.synthesizeKey("KEY_Escape");
await Services.fog.testFlushAllChildren();
Assert.equal(Glean.pdfjs.editing.print.testGetValue(), N);
Assert.equal(Glean.pdfjsEditingHighlight.print.testGetValue(), N);
Assert.equal(
Glean.pdfjsEditingHighlight.numberOfColors.one.testGetValue(),
N
);
}
await click(
browser,
"#highlightParamsToolbarContainer button[title='Green']"
);
const spanBox = await getSpanBox(browser, "Mozilla automated testing");
await BrowserTestUtils.synthesizeMouseAtPoint(
spanBox.x - 10,
spanBox.y + spanBox.height / 2,
{
type: "mousedown",
button: 0,
},
browser
);
await BrowserTestUtils.synthesizeMouseAtPoint(
spanBox.x + spanBox.width,
spanBox.y + spanBox.height / 2,
{
type: "mousemove",
button: 0,
},
browser
);
await BrowserTestUtils.synthesizeMouseAtPoint(
spanBox.x + spanBox.width,
spanBox.y + spanBox.height / 2,
{
type: "mouseup",
button: 0,
},
browser
);
await waitForEditors(browser, ".highlightEditor", 3);
await Services.fog.testFlushAllChildren();
Assert.equal(Glean.pdfjsEditingHighlight.color.green.testGetValue(), 1);
Assert.equal(
Glean.pdfjsEditingHighlight.kind.free_highlight.testGetValue(),
1
);
let telemetryPromise = waitForTelemetry(browser);
await focus(browser, "#editorFreeHighlightThickness");
await EventUtils.synthesizeKey("KEY_ArrowRight");
await telemetryPromise;
await Services.fog.testFlushAllChildren();
Assert.equal(
Glean.pdfjsEditingHighlight.thickness.testGetValue().values[12],
1
);
Assert.equal(
Glean.pdfjsEditingHighlight.thickness.testGetValue().values[13],
1
);
Assert.equal(
Glean.pdfjsEditingHighlight.thicknessChanged.testGetValue(),
1
);
document.getElementById("cmd_print").doCommand();
await BrowserTestUtils.waitForCondition(() => {
let preview = document.querySelector(".printPreviewBrowser");
return preview && BrowserTestUtils.isVisible(preview);
});
await EventUtils.synthesizeKey("KEY_Escape");
await Services.fog.testFlushAllChildren();
Assert.equal(Glean.pdfjs.editing.print.testGetValue(), 3);
Assert.equal(Glean.pdfjsEditingHighlight.print.testGetValue(), 3);
Assert.equal(
Glean.pdfjsEditingHighlight.numberOfColors.one.testGetValue(),
2
);
Assert.equal(
Glean.pdfjsEditingHighlight.numberOfColors.two.testGetValue(),
1
);
await click(browser, ".highlightEditor.free button.colorPicker");
telemetryPromise = waitForTelemetry(browser);
await click(
browser,
".highlightEditor.free button.colorPicker button[title='Red']"
);
await telemetryPromise;
await Services.fog.testFlushAllChildren();
Assert.equal(Glean.pdfjsEditingHighlight.colorChanged.testGetValue(), 1);
document.getElementById("cmd_print").doCommand();
await BrowserTestUtils.waitForCondition(() => {
let preview = document.querySelector(".printPreviewBrowser");
return preview && BrowserTestUtils.isVisible(preview);
});
await EventUtils.synthesizeKey("KEY_Escape");
await Services.fog.testFlushAllChildren();
Assert.equal(
Glean.pdfjsEditingHighlight.numberOfColors.one.testGetValue(),
2
);
Assert.equal(
Glean.pdfjsEditingHighlight.numberOfColors.two.testGetValue(),
2
);
telemetryPromise = waitForTelemetry(browser);
await EventUtils.synthesizeKey("KEY_Delete");
await telemetryPromise;
await Services.fog.testFlushAllChildren();
Assert.equal(Glean.pdfjsEditingHighlight.deleted.testGetValue(), 1);
await SpecialPowers.spawn(browser, [], async function () {
const viewer = content.wrappedJSObject.PDFViewerApplication;
viewer.pdfDocument.annotationStorage.resetModified();
await viewer.close();
});
}
);
});

View file

@ -119,6 +119,7 @@ async function waitForSelector(browser, selector, message) {
}
async function click(browser, selector) {
await waitForSelector(browser, selector);
await SpecialPowers.spawn(browser, [selector], async function (sel) {
const el = content.document.querySelector(sel);
await new Promise(r => {
@ -128,6 +129,17 @@ async function click(browser, selector) {
});
}
async function waitForTelemetry(browser) {
await BrowserTestUtils.waitForContentEvent(
browser,
"reporttelemetry",
false,
null,
true
);
await TestUtils.waitForTick();
}
/**
* Enable an editor (Ink, FreeText, ...).
* @param {Object} browser
@ -160,33 +172,40 @@ async function enableEditor(browser, name) {
* @param {string} text
* @returns {Object} the bbox of the span containing the text.
*/
async function getSpanBox(browser, text) {
return SpecialPowers.spawn(browser, [text], async function (text) {
const { ContentTaskUtils } = ChromeUtils.importESModule(
"resource://testing-common/ContentTaskUtils.sys.mjs"
);
const { document } = content;
async function getSpanBox(browser, text, pageNumber = 1) {
return SpecialPowers.spawn(
browser,
[text, pageNumber],
async function (text, number) {
const { ContentTaskUtils } = ChromeUtils.importESModule(
"resource://testing-common/ContentTaskUtils.sys.mjs"
);
const { document } = content;
await ContentTaskUtils.waitForCondition(
() => !!document.querySelector(".textLayer .endOfContent"),
"The text layer must be displayed"
);
await ContentTaskUtils.waitForCondition(
() =>
!!document.querySelector(
`.page[data-page-number='${number}'] .textLayer .endOfContent`
),
"The text layer must be displayed"
);
let targetSpan = null;
for (const span of document.querySelectorAll(
`.textLayer span[role="presentation"]`
)) {
if (span.innerText.includes(text)) {
targetSpan = span;
break;
let targetSpan = null;
for (const span of document.querySelectorAll(
`.page[data-page-number='${number}'] .textLayer span`
)) {
if (span.innerText.includes(text)) {
targetSpan = span;
break;
}
}
Assert.ok(!!targetSpan, `document must have a span containing '${text}'`);
const { x, y, width, height } = targetSpan.getBoundingClientRect();
return { x, y, width, height };
}
Assert.ok(targetSpan, `document must have a span containing '${text}'`);
const { x, y, width, height } = targetSpan.getBoundingClientRect();
return { x, y, width, height };
});
);
}
/**
@ -210,14 +229,16 @@ async function countElements(browser, selector) {
* @param {Object} browser
* @param {number} x
* @param {number} y
* @param {number} n
*/
async function clickAt(browser, x, y) {
async function clickAt(browser, x, y, n = 1) {
await BrowserTestUtils.synthesizeMouseAtPoint(
x,
y,
{
type: "mousedown",
button: 0,
clickCount: n,
},
browser
);
@ -227,6 +248,7 @@ async function clickAt(browser, x, y) {
{
type: "mouseup",
button: 0,
clickCount: n,
},
browser
);
@ -256,20 +278,30 @@ async function clickOn(browser, selector) {
await clickAt(browser, x, y);
}
async function focusEditorLayer(browser) {
return SpecialPowers.spawn(browser, [], async function () {
const layer = content.document.querySelector(".annotationEditorLayer");
if (layer === content.document.activeElement) {
function focusEditorLayer(browser) {
return focus(browser, ".annotationEditorLayer");
}
/**
* Focus an element corresponding to the given selector.
* @param {Object} browser
* @param {string} selector
* @returns
*/
async function focus(browser, selector) {
return SpecialPowers.spawn(browser, [selector], function (sel) {
const el = content.document.querySelector(sel);
if (el === content.document.activeElement) {
return Promise.resolve();
}
const promise = new Promise(resolve => {
const listener = () => {
layer.removeEventListener("focus", listener);
el.removeEventListener("focus", listener);
resolve();
};
layer.addEventListener("focus", listener);
el.addEventListener("focus", listener);
});
layer.focus();
el.focus();
return promise;
});
}
@ -317,14 +349,18 @@ async function addFreeText(browser, text, box) {
const count = await countElements(browser, ".freeTextEditor");
await focusEditorLayer(browser);
await clickAt(browser, x + 0.1 * width, y + 0.5 * height);
await BrowserTestUtils.waitForCondition(
async () => (await countElements(browser, ".freeTextEditor")) === count + 1
);
await waitForEditors(browser, ".freeTextEditor", count + 1);
await write(browser, text);
await escape(browser);
}
async function waitForEditors(browser, selector, count) {
await BrowserTestUtils.waitForCondition(
async () => (await countElements(browser, selector)) === count
);
}
function changeMimeHandler(preferredAction, alwaysAskBeforeHandling) {
let handlerService = Cc[
"@mozilla.org/uriloader/handler-service;1"