Bug 1970720 - Change tabId to browsingContext.topWindowContext.innerWindowId, and make general improvements a=dmeehan DONTBUILD

This patch unfortunately got mixed up with other stuff, so here's a list of changes this patch does.

- Fixing the tab id issue
- Ignoring invisible captchas for hCaptcha and Google reCAPTCHA
- Fixing double counting issue with multiple iframe captchas (again, hCaptcha and Google reCAPTCHA)
- Removes arkoselabs_ps(_pbm). We never recorded it, and `arkoselabs_ps === arkoselabs_oc`, so it is safe to remove it.

Original Revision: https://phabricator.services.mozilla.com/D252749

Differential Revision: https://phabricator.services.mozilla.com/D253496
This commit is contained in:
Fatih Kilic
2025-06-12 21:36:27 +00:00
committed by dmeehan@mozilla.com
parent 60bc6d9551
commit 111e33c73b
8 changed files with 137 additions and 152 deletions

View File

@@ -21,20 +21,20 @@ class CaptchaHandler {
/** /**
* @param {CaptchaDetectionChild} actor - The window actor. * @param {CaptchaDetectionChild} actor - The window actor.
* @param {Event} _event - The initial event that created the actor. * @param {Event} _event - The initial event that created the actor.
* @param {boolean} skipConstructedNotif - Whether to skip the constructed notification. Used for captchas with multiple frames.
*/ */
constructor(actor, _event) { constructor(actor, _event, skipConstructedNotif = false) {
/** @type {CaptchaDetectionChild} */ /** @type {CaptchaDetectionChild} */
this.actor = actor; this.actor = actor;
this.tabId = this.actor.docShell.browserChild.tabId; if (!skipConstructedNotif) {
this.isPBM = this.actor.browsingContext.usePrivateBrowsing; this.notifyConstructed();
this.notifyConstructed(); }
} }
notifyConstructed() { notifyConstructed() {
lazy.console.debug(`CaptchaHandler constructed: ${this.constructor.type}`); lazy.console.debug(`CaptchaHandler constructed: ${this.constructor.type}`);
this.actor.sendAsyncMessage("CaptchaHandler:Constructed", { this.actor.sendAsyncMessage("CaptchaHandler:Constructed", {
type: this.constructor.type, type: this.constructor.type,
isPBM: this.isPBM,
}); });
} }
@@ -43,11 +43,7 @@ class CaptchaHandler {
} }
updateState(state) { updateState(state) {
this.actor.sendAsyncMessage("CaptchaState:Update", { this.actor.sendAsyncMessage("CaptchaState:Update", state);
tabId: this.tabId,
isPBM: this.isPBM,
state,
});
} }
onActorDestroy() { onActorDestroy() {
@@ -87,7 +83,15 @@ class GoogleRecaptchaV2Handler extends CaptchaHandler {
static type = "g-recaptcha-v2"; static type = "g-recaptcha-v2";
constructor(actor, event) { constructor(actor, event) {
super(actor, event); super(
actor,
event,
actor.document.location.pathname.endsWith("/bframe") ||
(Cu.isInAutomation &&
actor.document.location.pathname.endsWith(
"g_recaptcha_v2_checkbox.html"
))
);
this.#enabled = true; this.#enabled = true;
this.#mutationObserver = new this.actor.contentWindow.MutationObserver( this.#mutationObserver = new this.actor.contentWindow.MutationObserver(
this.#mutationHandler.bind(this) this.#mutationHandler.bind(this)
@@ -109,10 +113,13 @@ class GoogleRecaptchaV2Handler extends CaptchaHandler {
); );
} }
return [ return (
"https://www.google.com/recaptcha/api2/", [
"https://www.google.com/recaptcha/enterprise/", "https://www.google.com/recaptcha/api2/",
].some(match => document.location.href.startsWith(match)); "https://www.google.com/recaptcha/enterprise/",
].some(match => document.location.href.startsWith(match)) &&
!document.location.search.includes("size=invisible")
);
} }
#mutationHandler(_mutations, observer) { #mutationHandler(_mutations, observer) {
@@ -327,17 +334,19 @@ class HCaptchaHandler extends CaptchaHandler {
static type = "hCaptcha"; static type = "hCaptcha";
constructor(actor, event) { constructor(actor, event) {
super(actor, event);
let params = null; let params = null;
try { try {
params = new URLSearchParams(this.actor.document.location.hash.slice(1)); params = new URLSearchParams(actor.document.location.hash.slice(1));
} catch { } catch {
// invalid URL // invalid URL
super(actor, event, true);
return; return;
} }
const frameType = params.get("frame"); const frameType = params.get("frame");
super(actor, event, frameType === "challenge");
if (frameType === "challenge") { if (frameType === "challenge") {
this.#initChallengeHandler(); this.#initChallengeHandler();
} else if (frameType === "checkbox") { } else if (frameType === "checkbox") {
@@ -357,7 +366,10 @@ class HCaptchaHandler extends CaptchaHandler {
return ( return (
document.location.href.startsWith( document.location.href.startsWith(
"https://newassets.hcaptcha.com/captcha/v1/" "https://newassets.hcaptcha.com/captcha/v1/"
) && document.location.pathname.endsWith("/static/hcaptcha.html") ) &&
document.location.pathname.endsWith("/static/hcaptcha.html") &&
!document.location.hash.includes("size=invisible") &&
!document.location.hash.includes("frame=checkbox-invisible")
); );
} }
@@ -507,9 +519,7 @@ export class CaptchaDetectionChild extends JSWindowActorChild {
} }
if (event.type === "pagehide") { if (event.type === "pagehide") {
this.sendAsyncMessage("TabState:Closed", { this.sendAsyncMessage("Page:Hide");
tabId: this.docShell.browserChild.tabId,
});
} }
this.handler?.handleEvent(event); this.handler?.handleEvent(event);

View File

@@ -18,7 +18,6 @@ ChromeUtils.defineLazyGetter(lazy, "console", () => {
export class CaptchaDetectionCommunicationChild extends JSWindowActorChild { export class CaptchaDetectionCommunicationChild extends JSWindowActorChild {
actorCreated() { actorCreated() {
lazy.console.debug("actorCreated()"); lazy.console.debug("actorCreated()");
this.tabId = this.docShell.browserChild.tabId;
this.addedMessageListener = false; this.addedMessageListener = false;
} }
@@ -52,23 +51,15 @@ export class CaptchaDetectionCommunicationChild extends JSWindowActorChild {
if (data.eventType === "load" && data.hasOwnProperty("responseType")) { if (data.eventType === "load" && data.hasOwnProperty("responseType")) {
this.sendAsyncMessage("CaptchaState:Update", { this.sendAsyncMessage("CaptchaState:Update", {
tabId: this.tabId, type: "datadome",
isPBM: this.browsingContext.usePrivateBrowsing, event: "load",
state: { captchaShown: data.responseType === "captcha",
type: "datadome", blocked: data.responseType === "hardblock",
event: "load",
captchaShown: data.responseType === "captcha",
blocked: data.responseType === "hardblock",
},
}); });
} else if (data.eventType === "passed") { } else if (data.eventType === "passed") {
this.sendAsyncMessage("CaptchaState:Update", { this.sendAsyncMessage("CaptchaState:Update", {
tabId: this.tabId, type: "datadome",
isPBM: this.browsingContext.usePrivateBrowsing, event: "passed",
state: {
type: "datadome",
event: "passed",
},
}); });
} }
}); });

View File

@@ -20,11 +20,12 @@ ChromeUtils.defineESModuleGetters(lazy, {
}); });
/** /**
* Holds the state of each tab. * Holds the state of captchas for each top document.
* Currently, only used by google reCAPTCHA v2 and hCaptcha.
* The state is an object with the following structure: * The state is an object with the following structure:
* [key: tabId]: typeof ReturnType<TabState.#defaultValue()> * [key: topBrowsingContextId]: typeof ReturnType<TopDocState.#defaultValue()>
*/ */
class TabState { class DocCaptchaState {
#state; #state;
constructor() { constructor() {
@@ -32,11 +33,11 @@ class TabState {
} }
/** /**
* @param {number} tabId - The tab id. * @param {number} topId - The top bc id.
* @returns {Map<any, any>} - The state of the tab. * @returns {Map<any, any>} - The state of the top bc.
*/ */
get(tabId) { get(topId) {
return this.#state.get(tabId); return this.#state.get(topId);
} }
static #defaultValue() { static #defaultValue() {
@@ -44,25 +45,25 @@ class TabState {
} }
/** /**
* @param {number} tabId - The tab id. * @param {number} topId - The top bc id.
* @param {(state: ReturnType<TabState['get']>) => void} updateFunction - The function to update the state. * @param {(state: ReturnType<DocCaptchaState['get']>) => void} updateFunction - The function to update the state.
*/ */
update(tabId, updateFunction) { update(topId, updateFunction) {
if (!this.#state.has(tabId)) { if (!this.#state.has(topId)) {
this.#state.set(tabId, TabState.#defaultValue()); this.#state.set(topId, DocCaptchaState.#defaultValue());
} }
updateFunction(this.#state.get(tabId)); updateFunction(this.#state.get(topId));
} }
/** /**
* @param {number} tabId - The tab id. * @param {number} topId - The top doc id.
*/ */
clear(tabId) { clear(topId) {
this.#state.delete(tabId); this.#state.delete(topId);
} }
} }
const tabState = new TabState(); const docState = new DocCaptchaState();
/** /**
* This actor parent is responsible for recording the state of captchas * This actor parent is responsible for recording the state of captchas
@@ -78,17 +79,18 @@ class CaptchaDetectionParent extends JSWindowActorParent {
actorDestroy() { actorDestroy() {
lazy.console.debug("actorDestroy()"); lazy.console.debug("actorDestroy()");
if (this.#responseObserver) { this.#onPageHidden();
this.#responseObserver.unregister();
}
} }
/** @type {CaptchaStateUpdateFunction} */ /** @type {CaptchaStateUpdateFunction} */
#updateGRecaptchaV2State({ tabId, isPBM, state: { type, changes } }) { #updateGRecaptchaV2State({ changes, type }) {
lazy.console.debug("updateGRecaptchaV2State", changes); lazy.console.debug("updateGRecaptchaV2State", changes);
const topId = this.#topInnerWindowId;
const isPBM = this.browsingContext.usePrivateBrowsing;
if (changes === "ImagesShown") { if (changes === "ImagesShown") {
tabState.update(tabId, state => { docState.update(topId, state => {
state.set(type + changes, true); state.set(type + changes, true);
}); });
@@ -99,28 +101,29 @@ class CaptchaDetectionParent extends JSWindowActorParent {
const shownMetric = "googleRecaptchaV2Ps" + (isPBM ? "Pbm" : ""); const shownMetric = "googleRecaptchaV2Ps" + (isPBM ? "Pbm" : "");
Glean.captchaDetection[shownMetric].add(1); Glean.captchaDetection[shownMetric].add(1);
} else if (changes === "GotCheckmark") { } else if (changes === "GotCheckmark") {
const autoCompleted = !tabState.get(tabId)?.has(type + "ImagesShown"); const autoCompleted = !docState.get(topId)?.has(type + "ImagesShown");
lazy.console.debug(
"GotCheckmark" +
(autoCompleted ? " (auto-completed)" : " (manually-completed)")
);
const resultMetric = const resultMetric =
"googleRecaptchaV2" + "googleRecaptchaV2" +
(autoCompleted ? "Ac" : "Pc") + (autoCompleted ? "Ac" : "Pc") +
(isPBM ? "Pbm" : ""); (isPBM ? "Pbm" : "");
Glean.captchaDetection[resultMetric].add(1); Glean.captchaDetection[resultMetric].add(1);
lazy.console.debug("Incremented metric", resultMetric);
docState.clear(topId);
this.#onMetricSet(); this.#onMetricSet();
} }
} }
/** @type {CaptchaStateUpdateFunction} */ /** @type {CaptchaStateUpdateFunction} */
#recordCFTurnstileResult({ isPBM, state: { result } }) { #recordCFTurnstileResult({ result }) {
lazy.console.debug("recordCFTurnstileResult", result); lazy.console.debug("recordCFTurnstileResult", result);
const isPBM = this.browsingContext.usePrivateBrowsing;
const resultMetric = const resultMetric =
"cloudflareTurnstile" + "cloudflareTurnstile" +
(result === "Succeeded" ? "Cc" : "Cf") + (result === "Succeeded" ? "Cc" : "Cf") +
(isPBM ? "Pbm" : ""); (isPBM ? "Pbm" : "");
Glean.captchaDetection[resultMetric].add(1); Glean.captchaDetection[resultMetric].add(1);
lazy.console.debug("Incremented metric", resultMetric);
this.#onMetricSet(); this.#onMetricSet();
} }
@@ -147,30 +150,42 @@ class CaptchaDetectionParent extends JSWindowActorParent {
} }
/** @type {CaptchaStateUpdateFunction} */ /** @type {CaptchaStateUpdateFunction} */
#recordDatadomeEvent({ isPBM, state: { event, ...payload } }) { #recordDatadomeEvent({ event, ...payload }) {
lazy.console.debug("recordDatadomeEvent", event, payload); lazy.console.debug("recordDatadomeEvent", { event, payload });
const suffix = isPBM ? "Pbm" : "";
const suffix = this.browsingContext.usePrivateBrowsing ? "Pbm" : "";
let metricName = "datadome";
if (event === "load") { if (event === "load") {
if (payload.captchaShown) { if (payload.captchaShown) {
Glean.captchaDetection["datadomePs" + suffix].add(1); metricName += "Ps";
} else if (payload.blocked) { } else if (payload.blocked) {
Glean.captchaDetection["datadomeBl" + suffix].add(1); metricName += "Bl";
} }
} else if (event === "passed") { } else if (event === "passed") {
Glean.captchaDetection["datadomePc" + suffix].add(1); metricName += "Pc";
} else {
lazy.console.error("Unknown Datadome event", event);
return;
} }
metricName += suffix;
Glean.captchaDetection[metricName].add(1);
lazy.console.debug("Incremented metric", metricName);
this.#onMetricSet(0); this.#onMetricSet(0);
} }
/** @type {CaptchaStateUpdateFunction} */ /** @type {CaptchaStateUpdateFunction} */
#recordHCaptchaState({ isPBM, tabId, state: { type, changes } }) { #recordHCaptchaState({ changes, type }) {
lazy.console.debug("recordHCaptchaEvent", changes); lazy.console.debug("recordHCaptchaEvent", changes);
const topId = this.#topInnerWindowId;
const isPBM = this.browsingContext.usePrivateBrowsing;
if (changes === "shown") { if (changes === "shown") {
// I don't think HCaptcha supports auto-completion, but we act // I don't think HCaptcha supports auto-completion, but we act
// as if it does just in case. // as if it does just in case.
tabState.update(tabId, state => { docState.update(topId, state => {
state.set(type + changes, true); state.set(type + changes, true);
}); });
@@ -180,38 +195,40 @@ class CaptchaDetectionParent extends JSWindowActorParent {
// received, or when the daily maybeSubmitPing is called. // received, or when the daily maybeSubmitPing is called.
const shownMetric = "hcaptchaPs" + (isPBM ? "Pbm" : ""); const shownMetric = "hcaptchaPs" + (isPBM ? "Pbm" : "");
Glean.captchaDetection[shownMetric].add(1); Glean.captchaDetection[shownMetric].add(1);
lazy.console.debug("Incremented metric", shownMetric);
} else if (changes === "passed") { } else if (changes === "passed") {
const autoCompleted = !tabState.get(tabId)?.has(type + "shown"); const autoCompleted = !docState.get(topId)?.has(type + "shown");
const resultMetric = const resultMetric =
"hcaptcha" + (autoCompleted ? "Ac" : "Pc") + (isPBM ? "Pbm" : ""); "hcaptcha" + (autoCompleted ? "Ac" : "Pc") + (isPBM ? "Pbm" : "");
Glean.captchaDetection[resultMetric].add(1); Glean.captchaDetection[resultMetric].add(1);
lazy.console.debug("Incremented metric", resultMetric);
docState.clear(topId);
this.#onMetricSet(); this.#onMetricSet();
} }
} }
/** @type {CaptchaStateUpdateFunction} */ /** @type {CaptchaStateUpdateFunction} */
#recordArkoseLabsEvent({ #recordArkoseLabsEvent({ event, solved, solutionsSubmitted }) {
isPBM, lazy.console.debug("recordArkoseLabsEvent", {
state: { event, solved, solutionsSubmitted }, event,
}) { solved,
if (event === "shown") { solutionsSubmitted,
// We don't call maybeSubmitPing here because we might end up });
// submitting the ping without the "completed" event.
// maybeSubmitPing will be called when "completed" event is
// received, or when the daily maybeSubmitPing is called.
const shownMetric = "arkoselabsPs" + (isPBM ? "Pbm" : "");
Glean.captchaDetection[shownMetric].add(1);
} else if (event === "completed") {
const suffix = isPBM ? "Pbm" : "";
const resultMetric = "arkoselabs" + (solved ? "Pc" : "Pf") + suffix;
Glean.captchaDetection[resultMetric].add(1);
const solutionsRequiredMetric = const isPBM = this.browsingContext.usePrivateBrowsing;
Glean.captchaDetection["arkoselabsSolutionsRequired" + suffix];
solutionsRequiredMetric.accumulateSingleSample(solutionsSubmitted);
this.#onMetricSet(); const suffix = isPBM ? "Pbm" : "";
} const resultMetric = "arkoselabs" + (solved ? "Pc" : "Pf") + suffix;
Glean.captchaDetection[resultMetric].add(1);
lazy.console.debug("Incremented metric", resultMetric);
const metricName = "arkoselabsSolutionsRequired" + suffix;
Glean.captchaDetection[metricName].accumulateSingleSample(
solutionsSubmitted
);
lazy.console.debug("Sampled", metricName, "with", solutionsSubmitted);
this.#onMetricSet();
} }
async #arkoseLabsInit() { async #arkoseLabsInit() {
@@ -259,17 +276,14 @@ class CaptchaDetectionParent extends JSWindowActorParent {
} }
solutionsSubmitted++; solutionsSubmitted++;
if (body.solved === null) { if (typeof body.solved !== "boolean") {
return; return;
} }
this.#recordArkoseLabsEvent({ this.#recordArkoseLabsEvent({
isPBM: this.browsingContext.usePrivateBrowsing, event: "completed",
state: { solved: body.solved,
event: "completed", solutionsSubmitted,
solved: body.solved,
solutionsSubmitted,
},
}); });
solutionsSubmitted = 0; solutionsSubmitted = 0;
@@ -278,8 +292,12 @@ class CaptchaDetectionParent extends JSWindowActorParent {
this.#responseObserver.register(); this.#responseObserver.register();
} }
#onTabClosed(tabId) { get #topInnerWindowId() {
tabState.clear(tabId); return this.browsingContext.topWindowContext.innerWindowId;
}
#onPageHidden() {
docState.clear(this.#topInnerWindowId);
if (this.#responseObserver) { if (this.#responseObserver) {
this.#responseObserver.unregister(); this.#responseObserver.unregister();
@@ -330,7 +348,9 @@ class CaptchaDetectionParent extends JSWindowActorParent {
await actor.sendQuery("Testing:MetricIsSet"); await actor.sendQuery("Testing:MetricIsSet");
} }
recordCaptchaHandlerConstructed({ isPBM, type }) { recordCaptchaHandlerConstructed({ type }) {
lazy.console.debug("recordCaptchaHandlerConstructed", type);
let metric = ""; let metric = "";
switch (type) { switch (type) {
case "g-recaptcha-v2": case "g-recaptcha-v2":
@@ -349,8 +369,9 @@ class CaptchaDetectionParent extends JSWindowActorParent {
metric = "arkoselabsOc"; metric = "arkoselabsOc";
break; break;
} }
metric += isPBM ? "Pbm" : ""; metric += this.browsingContext.usePrivateBrowsing ? "Pbm" : "";
Glean.captchaDetection[metric].add(1); Glean.captchaDetection[metric].add(1);
lazy.console.debug("Incremented metric", metric);
} }
async receiveMessage(message) { async receiveMessage(message) {
@@ -358,7 +379,7 @@ class CaptchaDetectionParent extends JSWindowActorParent {
switch (message.name) { switch (message.name) {
case "CaptchaState:Update": case "CaptchaState:Update":
switch (message.data.state.type) { switch (message.data.type) {
case "g-recaptcha-v2": case "g-recaptcha-v2":
this.#updateGRecaptchaV2State(message.data); this.#updateGRecaptchaV2State(message.data);
break; break;
@@ -376,17 +397,14 @@ class CaptchaDetectionParent extends JSWindowActorParent {
case "CaptchaHandler:Constructed": case "CaptchaHandler:Constructed":
// message.name === "CaptchaHandler:Constructed" // message.name === "CaptchaHandler:Constructed"
// => message.data = { // => message.data = {
// isPBM: bool,
// type: string, // type: string,
// } // }
this.recordCaptchaHandlerConstructed(message.data); this.recordCaptchaHandlerConstructed(message.data);
break; break;
case "TabState:Closed": case "Page:Hide":
// message.name === "TabState:Closed" // message.name === "TabState:Closed"
// => message.data = { // => message.data = undefined
// tabId: number, this.#onPageHidden();
// }
this.#onTabClosed(message.data.tabId);
break; break;
case "CaptchaDetection:Init": case "CaptchaDetection:Init":
// message.name === "CaptchaDetection:Init" // message.name === "CaptchaDetection:Init"
@@ -422,10 +440,8 @@ export {
/** /**
* @typedef CaptchaStateUpdateMessageData * @typedef CaptchaStateUpdateMessageData
* @property {number} tabId - The tab id. * @type {object}
* @property {boolean} isPBM - Whether the tab is in PBM. * @property {string} type - The type of the captcha.
* @property {object} state - The state of the captcha.
* @property {string} state.type - The type of the captcha.
* *
* @typedef {(message: CaptchaStateUpdateMessageData) => void} CaptchaStateUpdateFunction * @typedef {(message: CaptchaStateUpdateMessageData) => void} CaptchaStateUpdateFunction
*/ */

View File

@@ -279,22 +279,6 @@ captcha_detection:
data_sensitivity: data_sensitivity:
- interaction - interaction
arkoselabs_ps:
type: counter
description: >
How many times the ArkoseLabs challenge was shown.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1913421
data_reviews:
- https://phabricator.services.mozilla.com/D226021#7994299
notification_emails:
- tritter@mozilla.com
send_in_pings:
- captcha-detection
expires: never
data_sensitivity:
- interaction
arkoselabs_pc: arkoselabs_pc:
type: counter type: counter
description: > description: >
@@ -588,22 +572,6 @@ captcha_detection:
data_sensitivity: data_sensitivity:
- interaction - interaction
arkoselabs_ps_pbm:
type: counter
description: >
How many times the ArkoseLabs challenge was shown.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1913421
data_reviews:
- https://phabricator.services.mozilla.com/D226021#7994299
notification_emails:
- tritter@mozilla.com
send_in_pings:
- captcha-detection
expires: never
data_sensitivity:
- interaction
arkoselabs_pc_pbm: arkoselabs_pc_pbm:
type: counter type: counter
description: > description: >

View File

@@ -40,7 +40,7 @@
const occurredCount = const occurredCount =
await GleanTest.captchaDetection.googleRecaptchaV2Oc.testGetValue(); await GleanTest.captchaDetection.googleRecaptchaV2Oc.testGetValue();
is(occurredCount, 2, "We should have detected the occurrence"); is(occurredCount, 1, "We should have detected the occurrence");
await CaptchaTestingUtils.clearPrefs(); await CaptchaTestingUtils.clearPrefs();
}); });

View File

@@ -46,7 +46,7 @@
const occurredCount = const occurredCount =
await GleanTest.captchaDetection.googleRecaptchaV2Oc.testGetValue(); await GleanTest.captchaDetection.googleRecaptchaV2Oc.testGetValue();
is(occurredCount, 2, "We should have detected the occurrence"); is(occurredCount, 1, "We should have detected the occurrence");
await CaptchaTestingUtils.clearPrefs(); await CaptchaTestingUtils.clearPrefs();
}); });

View File

@@ -40,7 +40,7 @@
const occurredCount = const occurredCount =
await GleanTest.captchaDetection.hcaptchaOc.testGetValue(); await GleanTest.captchaDetection.hcaptchaOc.testGetValue();
is(occurredCount, 2, "We should have detected the occurrence"); is(occurredCount, 1, "We should have detected the occurrence");
await CaptchaTestingUtils.clearPrefs(); await CaptchaTestingUtils.clearPrefs();
}); });

View File

@@ -44,7 +44,7 @@
const occurredCount = const occurredCount =
await GleanTest.captchaDetection.hcaptchaOc.testGetValue(); await GleanTest.captchaDetection.hcaptchaOc.testGetValue();
is(occurredCount, 2, "We should have detected the occurrence"); is(occurredCount, 1, "We should have detected the occurrence");
await CaptchaTestingUtils.clearPrefs(); await CaptchaTestingUtils.clearPrefs();
}); });