summaryrefslogtreecommitdiffstats
path: root/toolkit/components/messaging-system/schemas/TriggerActionSchemas
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/messaging-system/schemas/TriggerActionSchemas')
-rw-r--r--toolkit/components/messaging-system/schemas/TriggerActionSchemas/TriggerActionSchemas.json261
-rw-r--r--toolkit/components/messaging-system/schemas/TriggerActionSchemas/index.md162
-rw-r--r--toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser.ini7
-rw-r--r--toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser_asrouter_trigger_docs.js74
-rw-r--r--toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser_asrouter_trigger_listeners.js501
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");
+});