From 806786f7134eec174f34c37742a2adc5a6306890 Mon Sep 17 00:00:00 2001 From: Andrei Oprea Date: Fri, 13 Sep 2019 12:47:40 +0000 Subject: [PATCH] Port 1570631 - Implement feature promotion doorhangers using CFR (#5308) --- common/Actions.jsm | 1 + .../templates/ExtensionDoorhanger.schema.json | 29 +++- lib/ASRouter.jsm | 4 + lib/ASRouterTargeting.jsm | 5 + lib/ASRouterTriggerListeners.jsm | 101 +++++++++++ lib/CFRMessageProvider.jsm | 139 +++++++++++++++ lib/CFRPageActions.jsm | 162 +++++++++++------- locales-src/asrouter.ftl | 13 ++ test/browser/browser_asrouter_cfr.js | 70 ++++++++ .../browser_asrouter_trigger_listeners.js | 94 ++++++++++ test/unit/asrouter/ASRouter.test.js | 12 ++ test/unit/asrouter/CFRMessageProvider.test.js | 4 +- test/unit/asrouter/constants.js | 4 +- .../templates/ExtensionDoorhanger.test.jsx | 2 + 14 files changed, 574 insertions(+), 66 deletions(-) diff --git a/common/Actions.jsm b/common/Actions.jsm index 1bcfeb9f1..ee2f3499e 100644 --- a/common/Actions.jsm +++ b/common/Actions.jsm @@ -161,6 +161,7 @@ for (const type of [ "SHOW_FIREFOX_ACCOUNTS", "PIN_CURRENT_TAB", "ENABLE_FIREFOX_MONITOR", + "OPEN_PROTECTION_PANEL", ]) { ASRouterActions[type] = type; } diff --git a/content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json b/content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json index fdfe3a87f..3deff64c0 100644 --- a/content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json +++ b/content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json @@ -15,6 +15,10 @@ } }, "properties": { + "layout": { + "type": "string", + "description": "The layout style of the pop-over." + }, "category": { "type": "string", "description": "Attribute used for different groups of messages from the same provider" @@ -24,10 +28,18 @@ "description": "Attribute used for different groups of messages from the same provider", "enum": ["message_and_animation", "icon_and_message", "addon_recommendation"] }, + "anchor_id": { + "type": "string", + "description": "A DOM element ID that the pop-over will be anchored." + }, "bucket_id": { "type": "string", "description": "A bucket identifier for the addon. This is used in order to anonymize telemetry for history-sensitive targeting." }, + "skip_address_bar_notifier": { + "type": "boolean", + "description": "Skip the 'Recommend' notifier and show directly." + }, "notification_text": { "description": "The text in the small blue chicklet that appears in the URL bar. This can be a reference to a localized string in Firefox or just a plain string.", "oneOf": [ @@ -88,6 +100,11 @@ } } }, + "learn_more": { + "type": "string", + "description": "Last part of the path in the SUMO URL to the support page with the information about the doorhanger.", + "examples": ["extensionpromotions", "extensionrecommendations"] + }, "heading_text": { "description": "The larger heading text displayed in the pop-over. This can be a reference to a localized string in Firefox or just a plain string.", "oneOf": [ @@ -108,12 +125,20 @@ ] }, "icon": { - "description": "The icon displayed in the pop-over. Should be 64x64px and png/svg.", + "description": "The icon displayed in the pop-over. Should be 32x32px or 64x64px and png/svg.", "allOf": [ {"$ref": "#/definitions/linkUrl"}, {"description": "Icon associated with the message"} ] }, + "icon_dark_theme": { + "type": "string", + "description": "Pop-over icon, dark theme variant. Should be 32x32px or 64x64px and png/svg." + }, + "icon_class": { + "type": "string", + "description": "CSS class of the pop-over icon." + }, "addon": { "description": "Addon information including AMO URL.", "type": "object", @@ -336,5 +361,5 @@ } }, "additionalProperties": false, - "required": ["category", "bucket_id", "notification_text", "heading_text", "text", "buttons"] + "required": ["layout", "category", "bucket_id", "notification_text", "heading_text", "text", "buttons"] } diff --git a/lib/ASRouter.jsm b/lib/ASRouter.jsm index 004e41f7b..12f43b282 100644 --- a/lib/ASRouter.jsm +++ b/lib/ASRouter.jsm @@ -1844,6 +1844,10 @@ class _ASRouter { csp: null, }); break; + case ra.OPEN_PROTECTION_PANEL: + let { gProtectionsHandler } = target.browser.ownerGlobal; + gProtectionsHandler.showProtectionsPopup({}); + break; } } diff --git a/lib/ASRouterTargeting.jsm b/lib/ASRouterTargeting.jsm index bd93be1f3..1f50720f2 100644 --- a/lib/ASRouterTargeting.jsm +++ b/lib/ASRouterTargeting.jsm @@ -488,8 +488,13 @@ this.ASRouterTargeting = { return ( (candidateMessageTrigger.params && + trigger.param.host && candidateMessageTrigger.params.includes(trigger.param.host)) || + (candidateMessageTrigger.params && + trigger.param.type && + candidateMessageTrigger.params.includes(trigger.param.type)) || (candidateMessageTrigger.patterns && + trigger.param.url && new MatchPatternSet(candidateMessageTrigger.patterns).matches( trigger.param.url )) diff --git a/lib/ASRouterTriggerListeners.jsm b/lib/ASRouterTriggerListeners.jsm index 14005059c..9b4fd10c0 100644 --- a/lib/ASRouterTriggerListeners.jsm +++ b/lib/ASRouterTriggerListeners.jsm @@ -429,6 +429,107 @@ this.ASRouterTriggerListeners = new Map([ }, }, ], + + /** + * Attach listener to count location changes and notify the trigger handler + * on content blocked event + */ + [ + "trackingProtection", + { + _initialized: false, + _triggerHandler: null, + _events: [], + _sessionPageLoad: 0, + onLocationChange: null, + + async init(triggerHandler, params, patterns) { + params.forEach(p => this._events.push(p)); + + if (!this._initialized) { + Services.obs.addObserver(this, "SiteProtection:ContentBlockingEvent"); + + this.onLocationChange = this._onLocationChange.bind(this); + + // Add listeners to all existing browser windows + for (let win of Services.wm.getEnumerator("navigator:browser")) { + if (isPrivateWindow(win)) { + continue; + } + await checkStartupFinished(win); + win.gBrowser.addTabsProgressListener(this); + } + + this._initialized = true; + } + this._triggerHandler = triggerHandler; + }, + + uninit() { + if (this._initialized) { + Services.obs.removeObserver( + this, + "SiteProtection:ContentBlockingEvent" + ); + + for (let win of Services.wm.getEnumerator("navigator:browser")) { + if (isPrivateWindow(win)) { + continue; + } + win.gBrowser.removeTabsProgressListener(this); + } + + this.onLocationChange = null; + this._initialized = false; + } + this._triggerHandler = null; + this._events = []; + this._sessionPageLoad = 0; + }, + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "SiteProtection:ContentBlockingEvent": + const { browser, host, event } = aSubject.wrappedJSObject; + if (this._events.includes(event)) { + this._triggerHandler(browser, { + id: "trackingProtection", + param: { + host, + type: event, + }, + context: { + pageLoad: this._sessionPageLoad, + }, + }); + } + break; + } + }, + + _onLocationChange( + aBrowser, + aWebProgress, + aRequest, + aLocationURI, + aFlags + ) { + // Some websites trigger redirect events after they finish loading even + // though the location remains the same. This results in onLocationChange + // events to be fired twice. + const isSameDocument = !!( + aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT + ); + if ( + ["http", "https"].includes(aLocationURI.scheme) && + aWebProgress.isTopLevel && + !isSameDocument + ) { + this._sessionPageLoad += 1; + } + }, + }, + ], ]); const EXPORTED_SYMBOLS = ["ASRouterTriggerListeners"]; diff --git a/lib/CFRMessageProvider.jsm b/lib/CFRMessageProvider.jsm index 450ac34fa..f1296ec86 100644 --- a/lib/CFRMessageProvider.jsm +++ b/lib/CFRMessageProvider.jsm @@ -546,6 +546,7 @@ const CFR_MESSAGES = [ string_id: "cfr-doorhanger-sync-logins-body", }, icon: "chrome://browser/content/aboutlogins/icons/intro-illustration.svg", + icon_class: "cfr-doorhanger-large-icon", buttons: { secondary: [ { @@ -604,6 +605,144 @@ const CFR_MESSAGES = [ id: "newSavedLogin", }, }, + { + id: "SOCIAL_TRACKING_PROTECTION", + template: "cfr_doorhanger", + content: { + layout: "icon_and_message", + category: "cfrFeatures", + anchor_id: "tracking-protection-icon-box", + skip_address_bar_notifier: true, + bucket_id: "CFR_SOCIAL_TRACKING_PROTECTION", + heading_text: { string_id: "cfr-doorhanger-socialtracking-heading" }, + notification_text: "", + info_icon: { + label: { + string_id: "cfr-doorhanger-extension-sumo-link", + }, + sumo_path: "extensionrecommendations", + }, + learn_more: "social-media-tracking-report", + text: { string_id: "cfr-doorhanger-socialtracking-description" }, + icon: "chrome://browser/skin/notification-icons/block-social.svg", + icon_dark_theme: + "chrome://browser/skin/notification-icons/block-social-dark.svg", + buttons: { + primary: { + label: { string_id: "cfr-doorhanger-socialtracking-ok-button" }, + action: { type: "OPEN_PROTECTION_PANEL" }, + event: "PROTECTION", + }, + secondary: [ + { + label: { string_id: "cfr-doorhanger-socialtracking-close-button" }, + event: "BLOCK", + }, + ], + }, + }, + targeting: "pageLoad >= 4", + frequency: { + lifetime: 2, + custom: [{ period: 2 * 86400 * 1000, cap: 1 }], + }, + trigger: { + id: "trackingProtection", + params: [Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT], + }, + }, + { + id: "FINGERPRINTERS_PROTECTION", + template: "cfr_doorhanger", + content: { + layout: "icon_and_message", + category: "cfrFeatures", + anchor_id: "tracking-protection-icon-box", + skip_address_bar_notifier: true, + bucket_id: "CFR_SOCIAL_TRACKING_PROTECTION", + heading_text: { string_id: "cfr-doorhanger-fingerprinters-heading" }, + notification_text: "", + info_icon: { + label: { + string_id: "cfr-doorhanger-extension-sumo-link", + }, + sumo_path: "extensionrecommendations", + }, + learn_more: "fingerprinters-report", + text: { string_id: "cfr-doorhanger-fingerprinters-description" }, + icon: "chrome://browser/skin/notification-icons/block-fingerprinter.svg", + icon_dark_theme: + "chrome://browser/skin/notification-icons/block-fingerprinter-dark.svg", + buttons: { + primary: { + label: { string_id: "cfr-doorhanger-socialtracking-ok-button" }, + action: { type: "OPEN_PROTECTION_PANEL" }, + event: "PROTECTION", + }, + secondary: [ + { + label: { string_id: "cfr-doorhanger-socialtracking-close-button" }, + event: "BLOCK", + }, + ], + }, + }, + targeting: "pageLoad >= 4", + frequency: { + lifetime: 2, + custom: [{ period: 2 * 86400 * 1000, cap: 1 }], + }, + trigger: { + id: "trackingProtection", + params: [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT], + }, + }, + { + id: "CRYPTOMINERS_PROTECTION", + template: "cfr_doorhanger", + content: { + layout: "icon_and_message", + category: "cfrFeatures", + anchor_id: "tracking-protection-icon-box", + skip_address_bar_notifier: true, + bucket_id: "CFR_SOCIAL_TRACKING_PROTECTION", + heading_text: { string_id: "cfr-doorhanger-cryptominers-heading" }, + notification_text: "", + info_icon: { + label: { + string_id: "cfr-doorhanger-extension-sumo-link", + }, + sumo_path: "extensionrecommendations", + }, + learn_more: "cryptominers-report", + text: { string_id: "cfr-doorhanger-cryptominers-description" }, + icon: "chrome://browser/skin/notification-icons/block-cryptominer.svg", + icon_dark_theme: + "chrome://browser/skin/notification-icons/block-cryptominer-dark.svg", + buttons: { + primary: { + label: { string_id: "cfr-doorhanger-socialtracking-ok-button" }, + action: { type: "OPEN_PROTECTION_PANEL" }, + event: "PROTECTION", + }, + secondary: [ + { + label: { string_id: "cfr-doorhanger-socialtracking-close-button" }, + event: "BLOCK", + }, + ], + }, + }, + targeting: "pageLoad >= 4", + frequency: { + lifetime: 2, + custom: [{ period: 2 * 86400 * 1000, cap: 1 }], + }, + trigger: { + id: "trackingProtection", + params: [Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT], + }, + }, ]; const CFRMessageProvider = { diff --git a/lib/CFRPageActions.jsm b/lib/CFRPageActions.jsm index c26823762..bb4e090e9 100644 --- a/lib/CFRPageActions.jsm +++ b/lib/CFRPageActions.jsm @@ -77,6 +77,31 @@ class PageAction { // Saved timeout IDs for scheduled state changes, so they can be cancelled this.stateTransitionTimeoutIDs = []; + + XPCOMUtils.defineLazyGetter(this, "isDarkTheme", () => { + try { + return this.window.document.documentElement.hasAttribute( + "lwt-toolbar-field-brighttext" + ); + } catch (e) { + return false; + } + }); + } + + addImpression(recommendation) { + this._dispatchImpression(recommendation); + // Only send an impression ping upon the first expansion. + // Note that when the user clicks on the "show" button on the asrouter admin + // page (both `bucket_id` and `id` will be set as null), we don't want to send + // the impression ping in that case. + if (!!recommendation.id && !!recommendation.content.bucket_id) { + this._sendTelemetry({ + message_id: recommendation.id, + bucket_id: recommendation.content.bucket_id, + event: "IMPRESSION", + }); + } } async showAddressBarNotifier(recommendation, shouldExpand = false) { @@ -114,18 +139,7 @@ class PageAction { // After one second, expand this._expand(DELAY_BEFORE_EXPAND_MS); - this._dispatchImpression(recommendation); - // Only send an impression ping upon the first expansion. - // Note that when the user clicks on the "show" button on the asrouter admin - // page (both `bucket_id` and `id` will be set as null), we don't want to send - // the impression ping in that case. - if (!!recommendation.id && !!recommendation.content.bucket_id) { - this._sendTelemetry({ - message_id: recommendation.id, - bucket_id: recommendation.content.bucket_id, - event: "IMPRESSION", - }); - } + this.addImpression(recommendation); } } @@ -473,6 +487,9 @@ class PageAction { this.window.document .getElementById("contextual-feature-recommendation-notification") .setAttribute("data-notification-category", content.layout); + this.window.document + .getElementById("contextual-feature-recommendation-notification") + .setAttribute("data-notification-bucket", content.bucket_id); switch (content.layout) { case "icon_and_message": @@ -491,10 +508,23 @@ class PageAction { }); RecommendationMap.delete(browser); }; + + let getIcon = () => { + if (content.icon_dark_theme && this.isDarkTheme) { + return content.icon_dark_theme; + } + return content.icon; + }; + + let learnMoreURL = content.learn_more + ? SUMO_BASE_URL + content.learn_more + : null; + panelTitle = await this.getStrings(content.heading_text); options = { - popupIconURL: content.icon, - popupIconClass: "cfr-doorhanger-large-icon", + popupIconURL: getIcon(), + popupIconClass: content.icon_class, + learnMoreURL, }; break; case "message_and_animation": @@ -579,52 +609,41 @@ class PageAction { callback: primaryActionCallback, }; - // For each secondary action, get the strings and attributes - const secondaryBtnStrings = []; - for (let button of secondary) { + let _renderSecondaryButtonAction = async (event, button) => { let label = await this.getStrings(button.label); - secondaryBtnStrings.push({ label, attributes: label.attributes }); - } - const secondaryActions = [ - { - label: secondaryBtnStrings[0].label, - accessKey: secondaryBtnStrings[0].attributes.accesskey, + let { attributes } = label; + + return { + label, + accessKey: attributes.accesskey, callback: () => { - this.dispatchUserAction(secondary[0].action); + if (button.action) { + this.dispatchUserAction(button.action); + } else { + this._blockMessage(id); + this.hideAddressBarNotifier(); + RecommendationMap.delete(browser); + } + this._sendTelemetry({ message_id: id, bucket_id: content.bucket_id, - event: "DISMISS", + event, }); }, - }, - { - label: secondaryBtnStrings[1].label, - accessKey: secondaryBtnStrings[1].attributes.accesskey, - callback: () => { - this._blockMessage(id); - this.hideAddressBarNotifier(); - this._sendTelemetry({ - message_id: id, - bucket_id: content.bucket_id, - event: "BLOCK", - }); - RecommendationMap.delete(browser); - }, - }, - { - label: secondaryBtnStrings[2].label, - accessKey: secondaryBtnStrings[2].attributes.accesskey, - callback: () => { - this.dispatchUserAction(secondary[2].action); - this._sendTelemetry({ - message_id: id, - bucket_id: content.bucket_id, - event: "MANAGE", - }); - }, - }, - ]; + }; + }; + + // For each secondary action, define default telemetry event + const defaultSecondaryEvent = ["DISMISS", "BLOCK", "MANAGE"]; + const secondaryActions = await Promise.all( + secondary.map((button, i) => { + return _renderSecondaryButtonAction( + button.event || defaultSecondaryEvent[i], + button + ); + }) + ); // Actually show the notification this.currentNotification = this.window.PopupNotifications.show( @@ -655,15 +674,23 @@ class PageAction { return; } const message = RecommendationMap.get(browser); - const { id, content } = message; // The recommendation should remain either collapsed or expanded while the // doorhanger is showing this._clearScheduledStateChanges(browser, message); + await this.showPopup(); + } + + async showPopup() { + const browser = this.window.gBrowser.selectedBrowser; + const message = RecommendationMap.get(browser); + const { id, content } = message; + // A hacky way of setting the popup anchor outside the usual url bar icon box // See https://searchfox.org/mozilla-central/rev/847b64cc28b74b44c379f9bff4f415b97da1c6d7/toolkit/modules/PopupNotifications.jsm#42 - browser.cfrpopupnotificationanchor = this.container; + browser.cfrpopupnotificationanchor = + this.window.document.getElementById(content.anchor_id) || this.container; this._sendTelemetry({ message_id: id, @@ -699,10 +726,11 @@ const CFRPageActions = { if (RecommendationMap.has(browser)) { const recommendation = RecommendationMap.get(browser); if ( - isHostMatch(browser, recommendation.host) || - // If there is no host associated we assume we're back on a tab - // that had a CFR message so we should show it again - !recommendation.host + !recommendation.content.skip_address_bar_notifier && + (isHostMatch(browser, recommendation.host) || + // If there is no host associated we assume we're back on a tab + // that had a CFR message so we should show it again + !recommendation.host) ) { // The browser has a recommendation specified with this host, so show // the page action @@ -762,7 +790,13 @@ const CFRPageActions = { if (!PageActionMap.has(win)) { PageActionMap.set(win, new PageAction(win, dispatchToASRouter)); } - await PageActionMap.get(win).showAddressBarNotifier(recommendation, true); + + if (content.skip_address_bar_notifier) { + await PageActionMap.get(win).showPopup(); + PageActionMap.get(win).addImpression(recommendation); + } else { + await PageActionMap.get(win).showAddressBarNotifier(recommendation, true); + } return true; }, @@ -795,7 +829,13 @@ const CFRPageActions = { if (!PageActionMap.has(win)) { PageActionMap.set(win, new PageAction(win, dispatchToASRouter)); } - await PageActionMap.get(win).showAddressBarNotifier(recommendation, true); + + if (content.skip_address_bar_notifier) { + await PageActionMap.get(win).showPopup(); + PageActionMap.get(win).addImpression(recommendation); + } else { + await PageActionMap.get(win).showAddressBarNotifier(recommendation, true); + } return true; }, diff --git a/locales-src/asrouter.ftl b/locales-src/asrouter.ftl index 5289ddb58..a7885a862 100644 --- a/locales-src/asrouter.ftl +++ b/locales-src/asrouter.ftl @@ -156,3 +156,16 @@ cfr-doorhanger-firefox-send-header = Share this PDF securely cfr-doorhanger-firefox-send-body = Keep your sensitive documents safe from prying eyes with end-to-end encryption and a link that disappears when you’re done. cfr-doorhanger-firefox-send-ok-button = Try { -send-brand-name } .accesskey = T + +## Social Tracking Protection + +cfr-doorhanger-socialtracking-ok-button = See Protections + .accesskey = P +cfr-doorhanger-socialtracking-close-button = Close + .accesskey = C +cfr-doorhanger-socialtracking-heading = { -brand-short-name } stopped a social network from tracking you here +cfr-doorhanger-socialtracking-description = Your privacy matters. { -brand-short-name } now blocks common social media trackers, limiting how much data they can collect about what you do online. +cfr-doorhanger-fingerprinters-heading = { -brand-short-name } blocked a fingerprinter on this page +cfr-doorhanger-fingerprinters-description = Your privacy matters. { -brand-short-name } now blocks fingerprinters, which collect pieces of uniquely identifiable information about your device to track you. +cfr-doorhanger-cryptominers-heading = { -brand-short-name } blocked a cryptominer on this page +cfr-doorhanger-cryptominers-description = Your privacy matters. { -brand-short-name } now blocks cryptominers, which use your system’s computing power to mine digital money. diff --git a/test/browser/browser_asrouter_cfr.js b/test/browser/browser_asrouter_cfr.js index 76d4dca59..9e10872f8 100644 --- a/test/browser/browser_asrouter_cfr.js +++ b/test/browser/browser_asrouter_cfr.js @@ -13,16 +13,22 @@ const createDummyRecommendation = ({ category, heading_text, layout, + skip_address_bar_notifier, }) => ({ content: { layout: layout || "addon_recommendation", category, + anchor_id: "page-action-buttons", + skip_address_bar_notifier, notification_text: "Mochitest", heading_text: heading_text || "Mochitest", info_icon: { label: { attributes: { tooltiptext: "Why am I seeing this" } }, sumo_path: "extensionrecommendations", }, + icon: "foo", + icon_dark_theme: "bar", + learn_more: "extensionrecommendations", addon: { id: "addon-id", title: "Addon name", @@ -107,6 +113,19 @@ function checkCFRAddonsElements(notification) { ); } +function checkCFRSocialTrackingProtection(notification) { + Assert.ok(notification.hidden === false, "Panel should be visible"); + Assert.ok( + notification.getAttribute("data-notification-category") === + "icon_and_message", + "Panel have corret data attribute" + ); + Assert.ok( + notification.querySelector("#cfr-notification-footer-learn-more-link"), + "Panel should have learn more link" + ); +} + function clearNotifications() { for (let notification of PopupNotifications._currentNotifications) { notification.remove(); @@ -128,6 +147,8 @@ function trigger_cfr_panel( heading_text, category = "cfrAddons", layout, + skip_address_bar_notifier = false, + use_single_secondary_button = false, } = {} ) { // a fake action type will result in the action being ignored @@ -136,10 +157,16 @@ function trigger_cfr_panel( category, heading_text, layout, + skip_address_bar_notifier, }); if (category !== "cfrAddons") { delete recommendation.content.addon; } + if (use_single_secondary_button) { + recommendation.content.buttons.secondary = [ + recommendation.content.buttons.secondary[0], + ]; + } clearNotifications(); @@ -400,6 +427,49 @@ add_task(async function test_cfr_pin_tab_notification_show() { ); }); +add_task( + async function test_cfr_social_tracking_protection_notification_show() { + // addRecommendation checks that scheme starts with http and host matches + let browser = gBrowser.selectedBrowser; + await BrowserTestUtils.loadURI(browser, "http://example.com/"); + await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/"); + + const showPanel = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + + const response = await trigger_cfr_panel(browser, "example.com", { + action: { type: "OPEN_PROTECTION_PANEL" }, + category: "cfrFeatures", + layout: "icon_and_message", + skip_address_bar_notifier: true, + use_single_secondary_button: true, + }); + Assert.ok( + response, + "Should return true if addRecommendation checks were successful" + ); + await showPanel; + + const notification = document.getElementById( + "contextual-feature-recommendation-notification" + ); + checkCFRSocialTrackingProtection(notification); + + // Check there is a primary button and click it. It will trigger the callback. + Assert.ok(notification.button); + let hidePanel = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + document + .getElementById("contextual-feature-recommendation-notification") + .button.click(); + await hidePanel; + } +); + add_task(async function test_cfr_features_and_addon_show() { // addRecommendation checks that scheme starts with http and host matches let browser = gBrowser.selectedBrowser; diff --git a/test/browser/browser_asrouter_trigger_listeners.js b/test/browser/browser_asrouter_trigger_listeners.js index 2ac05a053..38e78b2bc 100644 --- a/test/browser/browser_asrouter_trigger_listeners.js +++ b/test/browser/browser_asrouter_trigger_listeners.js @@ -141,3 +141,97 @@ add_task(async function check_newSavedLogin_listener() { } ); }); + +add_task(async function check_trackingProtection_listener() { + const TEST_URL = + "https://example.com/browser/browser/components/newtab/test/browser/red_page.html"; + + const contentBlockingEvent = 1234; + let observerEvent = 0; + let pageLoadSum = 0; + const triggerHandler = (target, trigger) => { + const { + id, + param: { host }, + context: { pageLoad }, + } = trigger; + is(id, "trackingProtection", "should match event name"); + is(host, TEST_URL, "should match test URL"); + + observerEvent += 1; + pageLoadSum += pageLoad; + }; + const trackingProtectionListener = ASRouterTriggerListeners.get( + "trackingProtection" + ); + + // Previously initialized by the Router + trackingProtectionListener.uninit(); + + // Initialise listener + await trackingProtectionListener.init(triggerHandler, [contentBlockingEvent]); + + await BrowserTestUtils.withNewTab( + TEST_URL, + async function triggerTrackingProtection(browser) { + Services.obs.notifyObservers( + { + wrappedJSObject: { + browser, + host: TEST_URL, + event: contentBlockingEvent + 1, + }, + }, + "SiteProtection:ContentBlockingEvent" + ); + } + ); + + is(observerEvent, 0, "shouldn't receive unrelated observer notification"); + is(pageLoadSum, 0, "shouldn't receive unrelated observer notification"); + + await BrowserTestUtils.withNewTab( + TEST_URL, + async function triggerTrackingProtection(browser) { + Services.obs.notifyObservers( + { + wrappedJSObject: { + browser, + host: TEST_URL, + event: contentBlockingEvent, + }, + }, + "SiteProtection:ContentBlockingEvent" + ); + + await BrowserTestUtils.waitForCondition( + () => observerEvent !== 0, + "Wait for the observer notification to run" + ); + is(observerEvent, 1, "should receive observer notification"); + is(pageLoadSum, 2, "should receive observer notification"); + } + ); + + // Uninitialise listener + trackingProtectionListener.uninit(); + + await BrowserTestUtils.withNewTab( + TEST_URL, + async function triggerTrackingProtectionAfterUninit(browser) { + Services.obs.notifyObservers( + { + wrappedJSObject: { + browser, + host: TEST_URL, + event: contentBlockingEvent, + }, + }, + "SiteProtection:ContentBlockingEvent" + ); + await new Promise(resolve => executeSoon(resolve)); + is(observerEvent, 1, "shouldn't receive obs. notification after uninit"); + is(pageLoadSum, 2, "shouldn't receive obs. notification after uninit"); + } + ); +}); diff --git a/test/unit/asrouter/ASRouter.test.js b/test/unit/asrouter/ASRouter.test.js index 9ba7c8c65..1b0037dc3 100644 --- a/test/unit/asrouter/ASRouter.test.js +++ b/test/unit/asrouter/ASRouter.test.js @@ -1971,6 +1971,18 @@ describe("ASRouter", () => { }); }); + describe("#onMessage: OPEN_PROTECTION_PANEL", () => { + it("should open protection panel", async () => { + const msg = fakeExecuteUserAction({ type: "OPEN_PROTECTION_PANEL" }); + let { gProtectionsHandler } = msg.target.browser.ownerGlobal; + + await Router.onMessage(msg); + + assert.calledOnce(gProtectionsHandler.showProtectionsPopup); + assert.calledWithExactly(gProtectionsHandler.showProtectionsPopup, {}); + }); + }); + describe("#dispatch(action, target)", () => { it("should an action and target to onMessage", async () => { // use the IMPRESSION action to make sure actions are actually getting processed diff --git a/test/unit/asrouter/CFRMessageProvider.test.js b/test/unit/asrouter/CFRMessageProvider.test.js index 6ad74057b..41e220abb 100644 --- a/test/unit/asrouter/CFRMessageProvider.test.js +++ b/test/unit/asrouter/CFRMessageProvider.test.js @@ -11,8 +11,8 @@ const REGULAR_IDS = [ ]; describe("CFRMessageProvider", () => { - it("should have a total of 5 messages", () => { - assert.lengthOf(messages, 5); + it("should have a total of 8 messages", () => { + assert.lengthOf(messages, 8); }); it("should have one message each for the three regular addons", () => { for (const id of REGULAR_IDS) { diff --git a/test/unit/asrouter/constants.js b/test/unit/asrouter/constants.js index aad2cfc01..006b2a18c 100644 --- a/test/unit/asrouter/constants.js +++ b/test/unit/asrouter/constants.js @@ -120,7 +120,6 @@ export const FAKE_RECOMMENDATION = { }, { label: { string_id: "secondary_button_id_2" }, - action: { id: "secondary_action" }, }, { label: { string_id: "secondary_button_id_3" }, @@ -150,6 +149,9 @@ export class FakeRemotePageManager { ConfirmationHint: { show: sinon.stub(), }, + gProtectionsHandler: { + showProtectionsPopup: sinon.stub(), + }, }, }; this.portID = "6000:2"; diff --git a/test/unit/asrouter/templates/ExtensionDoorhanger.test.jsx b/test/unit/asrouter/templates/ExtensionDoorhanger.test.jsx index ed3ed8e97..1841d3766 100644 --- a/test/unit/asrouter/templates/ExtensionDoorhanger.test.jsx +++ b/test/unit/asrouter/templates/ExtensionDoorhanger.test.jsx @@ -2,6 +2,7 @@ import { CFRMessageProvider } from "lib/CFRMessageProvider.jsm"; import schema from "content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json"; const DEFAULT_CONTENT = { + layout: "addon_recommendation", category: "dummyCategory", bucket_id: "some_bucket_id", notification_text: "Recommendation", @@ -40,6 +41,7 @@ const DEFAULT_CONTENT = { }; const L10N_CONTENT = { + layout: "addon_recommendation", category: "dummyL10NCategory", bucket_id: "some_bucket_id", notification_text: { string_id: "notification_text_id" },