diff options
Diffstat (limited to 'toolkit/components/messaging-system/schemas/TriggerActionSchemas')
5 files changed, 1005 insertions, 0 deletions
diff --git a/toolkit/components/messaging-system/schemas/TriggerActionSchemas/TriggerActionSchemas.json b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/TriggerActionSchemas.json new file mode 100644 index 0000000000..79f1637116 --- /dev/null +++ b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/TriggerActionSchemas.json @@ -0,0 +1,261 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/TriggerActionSchemas", + "definitions": { + "TriggerActionSchemas": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "openURL" + ] + }, + "params": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of urls we should match against" + }, + "patterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of Match pattern compatible strings to match against" + } + }, + "required": [ + "id" + ], + "additionalProperties": false, + "description": "Happens every time the user loads a new URL that matches the provided `hosts` or `patterns`" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "openArticleURL" + ] + }, + "params": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of urls we should match against" + }, + "patterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of Match pattern compatible strings to match against" + } + }, + "required": [ + "id" + ], + "additionalProperties": false, + "description": "Happens every time the user loads a document that is Reader Mode compatible" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "openBookmarkedURL" + ] + } + }, + "required": [ + "id" + ], + "additionalProperties": false, + "description": "Happens every time the user adds a bookmark from the URL bar star icon" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "frequentVisits" + ] + }, + "params": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of urls we should match against" + }, + "patterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of Match pattern compatible strings to match against" + } + }, + "required": [ + "id" + ], + "additionalProperties": false, + "description": "Happens every time a user navigates (or switches tab to) to any of the `hosts` or `patterns` arguments but additionally provides information about the number of accesses to the matched domain." + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "newSavedLogin" + ] + } + }, + "required": [ + "id" + ], + "additionalProperties": false, + "description": "Happens every time the user adds or updates a login" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "contentBlocking" + ] + }, + "params": { + "type": "array", + "items": { + "type": ["string", "integer"], + "description": "Events that should trigger this message. String values correspond to ContentBlockingMilestone events and number values correspond to STATE_BLOCKED_* flags on nsIWebProgressListener." + } + } + }, + "required": [ + "id", + "params" + ], + "additionalProperties": false, + "description": "Happens every time Firefox blocks the loading of a page script/asset/resource that matches the one of the tracking behaviours specifid through params. See https://searchfox.org/mozilla-central/rev/8ccea36c4fb09412609fb738c722830d7098602b/uriloader/base/nsIWebProgressListener.idl#336" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": ["defaultBrowserCheck"] + }, + "context": { + "type": "object", + "properties": { + "source": { + "type": "string", + "enum": ["newtab"], + "description": "When the source of the trigger is home/newtab" + }, + "willShowDefaultPrompt": { + "type": "boolean", + "description": "When the source of the trigger is startup" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": ["id"], + "description": "Happens when starting the browser or navigating to about:home/newtab" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": ["captivePortalLogin"] + } + }, + "additionalProperties": false, + "required": ["id"], + "description": "Happens when the user successfully goes through a captive portal authentication flow." + }, + { + "description": "Notify when a preference is added, removed or modified", + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": ["preferenceObserver"] + }, + "params": { + "type": "array", + "items": { + "type": "string", + "description": "Preference names to observe." + } + } + }, + "additionalProperties": false, + "required": ["id", "params"] + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": ["featureCalloutCheck"] + }, + "context": { + "type": "object", + "properties": { + "source": { + "type": "string", + "enum": ["firefoxview"], + "description": "Which about page is the source of the trigger" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": ["id"], + "description": "Happens when navigating to about:firefoxview or other about pages with Feature Callout tours enabled" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": ["nthTabClosed"] + } + }, + "additionalProperties": false, + "required": ["id"], + "description": "Happens when the user closes n or more tabs in a session" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": ["activityAfterIdle"] + } + }, + "additionalProperties": false, + "required": ["id"], + "description": "Happens when the user resumes activity after n milliseconds of inactivity (keyboard/mouse interactions and audio playback all count as activity)" + } + ] + } + } +} diff --git a/toolkit/components/messaging-system/schemas/TriggerActionSchemas/index.md b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/index.md new file mode 100644 index 0000000000..1443e7a681 --- /dev/null +++ b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/index.md @@ -0,0 +1,162 @@ +# Trigger Listeners + +A set of action listeners that can be used to trigger CFR messages. + +## Usage + +[As part of the CFR definition](https://searchfox.org/mozilla-central/rev/2bfe3415fb3a2fba9b1c694bc0b376365e086927/browser/components/newtab/lib/CFRMessageProvider.jsm#194) the message can register at most one trigger used to decide when the message is shown. + +Most triggers (unless otherwise specified) take the same arguments of `hosts` and/or `patterns` +used to target the message to specific websites. + +```javascript +// Optional set of hosts to filter out triggers only to certain websites +let params: string[]; +// Optional set of [match patterns](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns) to filter out triggers only to certain websites +let patterns: string[]; +``` + +```javascript +{ + ... + // Show the message when opening mozilla.org + "trigger": { "id": "openURL", "params": ["mozilla.org", "www.mozilla.org"] } + ... +} +``` + +```javascript +{ + ... + // Show the message when opening any HTTP, HTTPS URL. + trigger: { id: "openURL", patterns: ["*://*/*"] } + ... +} +``` + +## Available trigger actions + +### `openArticleURL` + +Happens when the user loads a Reader Mode compatible webpage. + +### `openBookmarkedURL` + +Happens when the user bookmarks or navigates to a bookmarked URL. + +Does not filter by host or patterns. + +### `frequentVisits` + +Happens every time a user navigates (or switches tab to) to any of the `hosts` or `patterns` arguments +provided. Additionally it stores timestamps of these visits that are provided back to the targeting context. +They can be used inside of the targeting expression: + +```javascript +// Has at least 3 visits in the past hour +recentVisits[.timestamp > (currentDate|date - 3600 * 1000 * 1)]|length >= 3 + +``` + +```typescript +interface visit { + host: string, + timestamp: UnixTimestamp +}; +// Host and timestamp for every visit to "Host" +let recentVisits: visit[]; +``` + +### `openURL` + +Happens every time the user loads a new URL that matches the provided `hosts` or `patterns`. +During a browsing session it keeps track of visits to unique urls that can be used inside targeting expression. + +```javascript +// True on the third visit for the URL which the trigger matched on +visitsCount >= 3 +``` + +### `newSavedLogin` + +Happens every time the user saves or updates a login via the login capture doorhanger. +Provides a `type` to diferentiate between the two events that can be used in targeting. + +Does not filter by host or patterns. + +```typescript +let type = "update" | "save"; +``` + +### `contentBlocking` + +Happens at the and of a document load and for every subsequent content blocked event, or when the tracking DB service hits a milestone. + +Provides a context of the number of pages loaded in the current browsing session that can be used in targeting. + +Does not filter by host or patterns. + +The event it reports back is one of two things: + * A combination of OR-ed [nsIWebProgressListener](https://searchfox.org/mozilla-central/source/uriloader/base/nsIWebProgressListener.idl) `STATE_BLOCKED_*` flags + * A string constants, such as [`"ContentBlockingMilestone"`](https://searchfox.org/mozilla-central/rev/8a2d8d26e25ef70c98c6036612aad534b76b9815/toolkit/components/antitracking/TrackingDBService.jsm#327-334) + + +### `defaultBrowserCheck` + +Happens at startup, when opening a newtab and when navigating to about:home. +At startup it provides the result of running `DefaultBrowserCheck.willCheckDefaultBrowser` to follow existing behaviour if needed. +On the newtab/homepage it reports the `source` as `newtab`. + +```typescript +let source = "newtab" | undefined; +let willShowDefaultPrompt = boolean; +``` + +### `captivePortalLogin` + +Happens when the user successfully goes through a captive portal authentication flow. + +### `preferenceObserver` + +Watch for changes on any number of preferences. Runs when a pref is added, removed or modified. + +```js +// Register a message with the following trigger +{ + id: "preferenceObserver", + params: ["pref name"] +} +``` + +### `featureCalloutCheck` + +Happens when navigating to about:firefoxview or other about pages with Feature Callout tours enabled + +### `nthTabClosed` + +Happens when the user closes n or more tabs in a session + +```js +// Register a message with the following trigger and +// include the tabsClosedCount context variable in the targeting. +// Here, the message triggers after two or more tabs are closed. +{ + trigger: { id: "nthTabClosed" }, + targeting: "tabsClosedCount >= 2" +} +``` + +### `activityAfterIdle` + +Happens when the user resumes activity after n milliseconds of inactivity. Keyboard/mouse interactions and audio playback count as activity. The idle timer is reset when the OS is put to sleep or wakes from sleep. + +No params or patterns. The `idleForMilliseconds` context variable is available in targeting. This value represents the number of milliseconds since the last user interaction or audio playback. `60000` is the minimum value for this variable (1 minute). In the following example, the message triggers when the user returns after at least 20 minutes of inactivity. + +```js +// Register a message with the following trigger and include +// the idleForMilliseconds context variable in the targeting. +{ + trigger: { id: "activityAfterIdle" }, + targeting: "idleForMilliseconds >= 1200000" +} +``` diff --git a/toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser.ini b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser.ini new file mode 100644 index 0000000000..51a90285d1 --- /dev/null +++ b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + ../../index.md + +[browser_asrouter_trigger_listeners.js] +https_first_disabled = true +[browser_asrouter_trigger_docs.js] diff --git a/toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser_asrouter_trigger_docs.js b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser_asrouter_trigger_docs.js new file mode 100644 index 0000000000..e2f038eda8 --- /dev/null +++ b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser_asrouter_trigger_docs.js @@ -0,0 +1,74 @@ +const TEST_URL = + "https://example.com/browser/toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/index.md"; + +const { ASRouterTriggerListeners } = ChromeUtils.import( + "resource://activity-stream/lib/ASRouterTriggerListeners.jsm" +); +const { CFRMessageProvider } = ChromeUtils.import( + "resource://activity-stream/lib/CFRMessageProvider.jsm" +); +const { JsonSchema } = ChromeUtils.importESModule( + "resource://gre/modules/JsonSchema.sys.mjs" +); + +XPCOMUtils.defineLazyGetter(this, "fetchTriggerActionSchema", async () => { + const response = await fetch( + "resource://testing-common/TriggerActionSchemas.json" + ); + const schema = await response.json(); + if (!schema) { + throw new Error("Failed to load TriggerActionSchemas"); + } + return schema.definitions.TriggerActionSchemas; +}); + +async function validateTrigger(trigger) { + const schema = await fetchTriggerActionSchema; + const result = JsonSchema.validate(trigger, schema); + if (result.errors.length) { + throw new Error( + `Trigger with id ${trigger.id} was not valid. Errors: ${JSON.stringify( + result.errors, + undefined, + 2 + )}` + ); + } + Assert.equal( + result.errors.length, + 0, + `should be a valid trigger of type ${trigger.id}` + ); +} + +function getHeadingsFromDocs(docs) { + const re = /### `(\w+)`/g; + const found = []; + let match = 1; + while (match) { + match = re.exec(docs); + if (match) { + found.push(match[1]); + } + } + return found; +} + +add_task(async function test_trigger_docs() { + let request = await fetch(TEST_URL, { credentials: "omit" }); + let docs = await request.text(); + let headings = getHeadingsFromDocs(docs); + for (let triggerName of ASRouterTriggerListeners.keys()) { + Assert.ok( + headings.includes(triggerName), + `${triggerName} not found in TriggerActionSchemas/index.md` + ); + } +}); + +add_task(async function test_message_triggers() { + const messages = await CFRMessageProvider.getMessages(); + for (let message of messages) { + await validateTrigger(message.trigger); + } +}); diff --git a/toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser_asrouter_trigger_listeners.js b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser_asrouter_trigger_listeners.js new file mode 100644 index 0000000000..12725fb4fb --- /dev/null +++ b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser_asrouter_trigger_listeners.js @@ -0,0 +1,501 @@ +ChromeUtils.defineModuleGetter( + this, + "ASRouterTriggerListeners", + "resource://activity-stream/lib/ASRouterTriggerListeners.jsm" +); +ChromeUtils.defineESModuleGetters(this, { + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", +}); + +async function openURLInWindow(window, url) { + const { selectedBrowser } = window.gBrowser; + BrowserTestUtils.loadURI(selectedBrowser, url); + await BrowserTestUtils.browserLoaded(selectedBrowser, false, url); +} + +add_task(async function check_matchPatternFailureCase() { + const articleTrigger = ASRouterTriggerListeners.get("openArticleURL"); + + articleTrigger.uninit(); + + articleTrigger.init(() => {}, [], ["example.com"]); + + is( + articleTrigger._matchPatternSet.matches("http://example.com"), + false, + "Should fail, bad pattern" + ); + + articleTrigger.init(() => {}, [], ["*://*.example.com/"]); + + is( + articleTrigger._matchPatternSet.matches("http://www.example.com"), + true, + "Should work, updated pattern" + ); + + articleTrigger.uninit(); +}); + +add_task(async function check_openArticleURL() { + const TEST_URL = + "https://example.com/browser/browser/components/newtab/test/browser/red_page.html"; + const articleTrigger = ASRouterTriggerListeners.get("openArticleURL"); + + // Previously initialized by the Router + articleTrigger.uninit(); + + // Initialize the trigger with a new triggerHandler that resolves a promise + // with the URL match + const listenerTriggered = new Promise(resolve => + articleTrigger.init((browser, match) => resolve(match), ["example.com"]) + ); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + await openURLInWindow(win, TEST_URL); + // Send a message from the content page (the TEST_URL) to the parent + // This should trigger the `receiveMessage` cb in the articleTrigger + await ContentTask.spawn(win.gBrowser.selectedBrowser, null, async () => { + let readerActor = content.windowGlobalChild.getActor("AboutReader"); + readerActor.sendAsyncMessage("Reader:UpdateReaderButton", { + isArticle: true, + }); + }); + + await listenerTriggered.then(data => + is( + data.param.url, + TEST_URL, + "We should match on the TEST_URL as a website article" + ) + ); + + // Cleanup + articleTrigger.uninit(); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function check_openURL_listener() { + const TEST_URL = + "https://example.com/browser/browser/components/newtab/test/browser/red_page.html"; + + let urlVisitCount = 0; + const triggerHandler = () => urlVisitCount++; + const openURLListener = ASRouterTriggerListeners.get("openURL"); + + // Previously initialized by the Router + openURLListener.uninit(); + + const normalWindow = await BrowserTestUtils.openNewBrowserWindow(); + const privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + // Initialise listener + openURLListener.init(triggerHandler, ["example.com"]); + + await openURLInWindow(normalWindow, TEST_URL); + await BrowserTestUtils.waitForCondition( + () => urlVisitCount !== 0, + "Wait for the location change listener to run" + ); + is(urlVisitCount, 1, "should receive page visits from existing windows"); + + await openURLInWindow(normalWindow, "http://www.example.com/abc"); + is(urlVisitCount, 1, "should not receive page visits for different domains"); + + await openURLInWindow(privateWindow, TEST_URL); + is( + urlVisitCount, + 1, + "should not receive page visits from existing private windows" + ); + + const secondNormalWindow = await BrowserTestUtils.openNewBrowserWindow(); + await openURLInWindow(secondNormalWindow, TEST_URL); + await BrowserTestUtils.waitForCondition( + () => urlVisitCount === 2, + "Wait for the location change listener to run" + ); + is(urlVisitCount, 2, "should receive page visits from newly opened windows"); + + const secondPrivateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await openURLInWindow(secondPrivateWindow, TEST_URL); + is( + urlVisitCount, + 2, + "should not receive page visits from newly opened private windows" + ); + + // Uninitialise listener + openURLListener.uninit(); + + await openURLInWindow(normalWindow, TEST_URL); + is( + urlVisitCount, + 2, + "should now not receive page visits from existing windows" + ); + + const thirdNormalWindow = await BrowserTestUtils.openNewBrowserWindow(); + await openURLInWindow(thirdNormalWindow, TEST_URL); + is( + urlVisitCount, + 2, + "should now not receive page visits from newly opened windows" + ); + + // Cleanup + const windows = [ + normalWindow, + privateWindow, + secondNormalWindow, + secondPrivateWindow, + thirdNormalWindow, + ]; + await Promise.all(windows.map(win => BrowserTestUtils.closeWindow(win))); +}); + +add_task(async function check_newSavedLogin_save_listener() { + const TEST_URL = + "https://example.com/browser/browser/components/newtab/test/browser/red_page.html"; + + let triggerTypesHandled = { + save: 0, + update: 0, + }; + const triggerHandler = (sub, { id, context }) => { + is(id, "newSavedLogin", "Check trigger id"); + triggerTypesHandled[context.type]++; + }; + const newSavedLoginListener = ASRouterTriggerListeners.get("newSavedLogin"); + + // Previously initialized by the Router + newSavedLoginListener.uninit(); + + // Initialise listener + await newSavedLoginListener.init(triggerHandler); + + await BrowserTestUtils.withNewTab( + TEST_URL, + async function triggerNewSavedPassword(browser) { + Services.obs.notifyObservers(browser, "LoginStats:NewSavedPassword"); + await BrowserTestUtils.waitForCondition( + () => triggerTypesHandled.save !== 0, + "Wait for the observer notification to run" + ); + is(triggerTypesHandled.save, 1, "should receive observer notification"); + } + ); + + is(triggerTypesHandled.update, 0, "shouldn't have handled other trigger"); + + // Uninitialise listener + newSavedLoginListener.uninit(); + + await BrowserTestUtils.withNewTab( + TEST_URL, + async function triggerNewSavedPasswordAfterUninit(browser) { + Services.obs.notifyObservers(browser, "LoginStats:NewSavedPassword"); + await new Promise(resolve => executeSoon(resolve)); + is( + triggerTypesHandled.save, + 1, + "shouldn't receive obs. notification after uninit" + ); + } + ); +}); + +add_task(async function check_newSavedLogin_update_listener() { + const TEST_URL = + "https://example.com/browser/browser/components/newtab/test/browser/red_page.html"; + + let triggerTypesHandled = { + save: 0, + update: 0, + }; + const triggerHandler = (sub, { id, context }) => { + is(id, "newSavedLogin", "Check trigger id"); + triggerTypesHandled[context.type]++; + }; + const newSavedLoginListener = ASRouterTriggerListeners.get("newSavedLogin"); + + // Previously initialized by the Router + newSavedLoginListener.uninit(); + + // Initialise listener + await newSavedLoginListener.init(triggerHandler); + + await BrowserTestUtils.withNewTab( + TEST_URL, + async function triggerLoginUpdateSaved(browser) { + Services.obs.notifyObservers(browser, "LoginStats:LoginUpdateSaved"); + await BrowserTestUtils.waitForCondition( + () => triggerTypesHandled.update !== 0, + "Wait for the observer notification to run" + ); + is(triggerTypesHandled.update, 1, "should receive observer notification"); + } + ); + + is(triggerTypesHandled.save, 0, "shouldn't have handled other trigger"); + + // Uninitialise listener + newSavedLoginListener.uninit(); + + await BrowserTestUtils.withNewTab( + TEST_URL, + async function triggerLoginUpdateSavedAfterUninit(browser) { + Services.obs.notifyObservers(browser, "LoginStats:LoginUpdateSaved"); + await new Promise(resolve => executeSoon(resolve)); + is( + triggerTypesHandled.update, + 1, + "shouldn't receive obs. notification after uninit" + ); + } + ); +}); + +add_task(async function check_contentBlocking_listener() { + const TEST_URL = + "https://example.com/browser/browser/components/newtab/test/browser/red_page.html"; + + const event1 = 0x0001; + const event2 = 0x0010; + const event3 = 0x0100; + const event4 = 0x1000; + + // Initialise listener to listen 2 events, for any incoming event e, + // it will be triggered if and only if: + // 1. (e & event1) && (e & event2) + // 2. (e & event3) + const bindEvents = [event1 | event2, event3]; + + let observerEvent = 0; + let pageLoadSum = 0; + const triggerHandler = (target, trigger) => { + const { + id, + param: { host, type }, + context: { pageLoad }, + } = trigger; + is(id, "contentBlocking", "should match event name"); + is(host, TEST_URL, "should match test URL"); + is( + bindEvents.filter(e => (type & e) === e).length, + 1, + `event ${type} is valid` + ); + ok(pageLoadSum <= pageLoad, "pageLoad is non-decreasing"); + + observerEvent += 1; + pageLoadSum = pageLoad; + }; + const contentBlockingListener = ASRouterTriggerListeners.get( + "contentBlocking" + ); + + // Previously initialized by the Router + contentBlockingListener.uninit(); + + await contentBlockingListener.init(triggerHandler, bindEvents); + + await BrowserTestUtils.withNewTab( + TEST_URL, + async function triggercontentBlocking(browser) { + Services.obs.notifyObservers( + { + wrappedJSObject: { + browser, + host: TEST_URL, + event: event1, // won't trigger + }, + }, + "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 triggercontentBlocking(browser) { + Services.obs.notifyObservers( + { + wrappedJSObject: { + browser, + host: TEST_URL, + event: event3, // will trigger + }, + }, + "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"); + + Services.obs.notifyObservers( + { + wrappedJSObject: { + browser, + host: TEST_URL, + event: event1 | event2 | event4, // still trigger + }, + }, + "SiteProtection:ContentBlockingEvent" + ); + + await BrowserTestUtils.waitForCondition( + () => observerEvent !== 1, + "Wait for the observer notification to run" + ); + is(observerEvent, 2, "should receive another observer notification"); + is(pageLoadSum, 2, "should receive another observer notification"); + + Services.obs.notifyObservers( + { + wrappedJSObject: { + browser, + host: TEST_URL, + event: event1, // no trigger + }, + }, + "SiteProtection:ContentBlockingEvent" + ); + + await new Promise(resolve => executeSoon(resolve)); + is(observerEvent, 2, "shouldn't receive unrelated notification"); + is(pageLoadSum, 2, "shouldn't receive unrelated notification"); + } + ); + + // Uninitialise listener + contentBlockingListener.uninit(); + + await BrowserTestUtils.withNewTab( + TEST_URL, + async function triggercontentBlockingAfterUninit(browser) { + Services.obs.notifyObservers( + { + wrappedJSObject: { + browser, + host: TEST_URL, + event: event3, // wont trigger after uninit + }, + }, + "SiteProtection:ContentBlockingEvent" + ); + await new Promise(resolve => executeSoon(resolve)); + is(observerEvent, 2, "shouldn't receive obs. notification after uninit"); + is(pageLoadSum, 2, "shouldn't receive obs. notification after uninit"); + } + ); +}); + +add_task(async function check_contentBlockingMilestone_listener() { + const TEST_URL = + "https://example.com/browser/browser/components/newtab/test/browser/red_page.html"; + + let observerEvent = 0; + const triggerHandler = (target, trigger) => { + const { + id, + param: { type }, + } = trigger; + is(id, "contentBlocking", "should match event name"); + is(type, "ContentBlockingMilestone", "Should be the correct event type"); + observerEvent += 1; + }; + const contentBlockingListener = ASRouterTriggerListeners.get( + "contentBlocking" + ); + + // Previously initialized by the Router + contentBlockingListener.uninit(); + + // Initialise listener + contentBlockingListener.init(triggerHandler, ["ContentBlockingMilestone"]); + + await BrowserTestUtils.withNewTab( + TEST_URL, + async function triggercontentBlocking(browser) { + Services.obs.notifyObservers( + { + wrappedJSObject: { + browser, + event: "Other Event", + }, + }, + "SiteProtection:ContentBlockingMilestone" + ); + } + ); + + is(observerEvent, 0, "shouldn't receive unrelated observer notification"); + + await BrowserTestUtils.withNewTab( + TEST_URL, + async function triggercontentBlocking(browser) { + Services.obs.notifyObservers( + { + wrappedJSObject: { + browser, + event: "ContentBlockingMilestone", + }, + }, + "SiteProtection:ContentBlockingMilestone" + ); + + await BrowserTestUtils.waitForCondition( + () => observerEvent !== 0, + "Wait for the observer notification to run" + ); + is(observerEvent, 1, "should receive observer notification"); + } + ); + + // Uninitialise listener + contentBlockingListener.uninit(); + + await BrowserTestUtils.withNewTab( + TEST_URL, + async function triggercontentBlockingAfterUninit(browser) { + Services.obs.notifyObservers( + { + wrappedJSObject: { + browser, + event: "ContentBlockingMilestone", + }, + }, + "SiteProtection:ContentBlockingMilestone" + ); + await new Promise(resolve => executeSoon(resolve)); + is(observerEvent, 1, "shouldn't receive obs. notification after uninit"); + } + ); +}); + +add_task(function test_pattern_match() { + const openURLListener = ASRouterTriggerListeners.get("openURL"); + openURLListener.uninit(); + openURLListener.init(() => {}, [], ["*://*/*.pdf"]); + let pattern = openURLListener._matchPatternSet; + + Assert.ok(pattern.matches("https://example.com/foo.pdf"), "match 1"); + Assert.ok(pattern.matches("https://example.com/bar/foo.pdf"), "match 2"); + Assert.ok(pattern.matches("https://www.example.com/foo.pdf"), "match 3"); + // Shouldn't match. Too generic. + Assert.ok(!pattern.matches("https://www.example.com/foo"), "match 4"); + Assert.ok(!pattern.matches("https://www.example.com/pdf"), "match 5"); +}); |