summaryrefslogtreecommitdiffstats
path: root/toolkit/components/messaging-system/schemas
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/messaging-system/schemas
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/messaging-system/schemas')
-rw-r--r--toolkit/components/messaging-system/schemas/NimbusExperiment.schema.json187
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/SpecialMessageActionSchemas.json396
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/index.md243
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser.ini23
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma.js21
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_accept_doh.js17
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_cancel.js14
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_cfrmessageprovider.js31
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_configure_homepage.js141
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_default_browser.js22
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_disable_doh.js28
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_docs.js30
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_about_page.js34
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_awesome_bar.js9
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_private_browser_window.js17
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_protection_panel.js22
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_protection_report.js29
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_url.js33
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_pin_current_tab.js14
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_show_firefox_accounts.js46
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_show_migration_wizard.js51
-rw-r--r--toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/head.js60
-rw-r--r--toolkit/components/messaging-system/schemas/TriggerActionSchemas/TriggerActionSchemas.json192
-rw-r--r--toolkit/components/messaging-system/schemas/TriggerActionSchemas/index.md121
-rw-r--r--toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser.ini6
-rw-r--r--toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser_asrouter_trigger_docs.js66
-rw-r--r--toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser_asrouter_trigger_listeners.js507
-rw-r--r--toolkit/components/messaging-system/schemas/index.rst18
28 files changed, 2378 insertions, 0 deletions
diff --git a/toolkit/components/messaging-system/schemas/NimbusExperiment.schema.json b/toolkit/components/messaging-system/schemas/NimbusExperiment.schema.json
new file mode 100644
index 0000000000..c52750f0b1
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/NimbusExperiment.schema.json
@@ -0,0 +1,187 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$ref": "#/definitions/NimbusExperiment",
+ "definitions": {
+ "NimbusExperiment": {
+ "type": "object",
+ "properties": {
+ "slug": {
+ "type": "string",
+ "description": "Unique identifier for the experiment"
+ },
+ "id": {
+ "type": "string",
+ "description": "Unique identifier for the experiment. This is a duplicate of slug, but is a required field\nfor all Remote Settings records."
+ },
+ "application": {
+ "type": "string",
+ "description": "A specific product such as Firefox Desktop or Fenix that supports Nimbus experiments"
+ },
+ "userFacingName": {
+ "type": "string",
+ "description": "Public name of the experiment displayed on \"about:studies\""
+ },
+ "userFacingDescription": {
+ "type": "string",
+ "description": "Short public description of the experiment displayed on on \"about:studies\""
+ },
+ "isEnrollmentPaused": {
+ "type": "boolean",
+ "description": "Should we enroll new users into the experiment?"
+ },
+ "bucketConfig": {
+ "type": "object",
+ "properties": {
+ "randomizationUnit": {
+ "type": "string",
+ "description": "A unique, stable identifier for the user used as an input to bucket hashing"
+ },
+ "namespace": {
+ "type": "string",
+ "description": "Additional inputs to the hashing function"
+ },
+ "start": {
+ "type": "number",
+ "description": "Index of start of the range of buckets"
+ },
+ "count": {
+ "type": "number",
+ "description": "Number of buckets to check"
+ },
+ "total": {
+ "type": "number",
+ "description": "Total number of buckets",
+ "default": 10000
+ }
+ },
+ "required": [
+ "randomizationUnit",
+ "namespace",
+ "start",
+ "count",
+ "total"
+ ],
+ "additionalProperties": false,
+ "description": "Bucketing configuration"
+ },
+ "probeSets": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "A list of probe set slugs relevant to the experiment analysis"
+ },
+ "branches": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "slug": {
+ "type": "string",
+ "description": "Identifier for the branch"
+ },
+ "ratio": {
+ "type": "number",
+ "description": "Relative ratio of population for the branch (e.g. if branch A=1 and branch B=3,\nbranch A would get 25% of the population)",
+ "default": 1
+ },
+ "feature": {
+ "type": "object",
+ "properties": {
+ "featureId": {
+ "type": "string",
+ "description": "The identifier for the feature flag"
+ },
+ "enabled": {
+ "type": "boolean",
+ "description": "This can be used to turn the whole feature on/off"
+ },
+ "value": {
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": {}
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional extra params for the feature (this should be validated against a schema)"
+ }
+ },
+ "required": [
+ "featureId",
+ "enabled",
+ "value"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "slug",
+ "ratio"
+ ],
+ "additionalProperties": false
+ },
+ "description": "Branch configuration for the experiment"
+ },
+ "targeting": {
+ "type": "string",
+ "description": "JEXL expression used to filter experiments based on locale, geo, etc."
+ },
+ "startDate": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Actual publish date of the experiment\nNote that this value is expected to be null in Remote Settings.",
+ "format": "date-time"
+ },
+ "endDate": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Actual end date of the experiment.\nNote that this value is expected to be null in Remote Settings.",
+ "format": "date-time"
+ },
+ "proposedDuration": {
+ "type": "number",
+ "description": "Duration of the experiment from the start date in days.\nNote that this value is expected to be null in Remote Settings.\nin Remote Settings."
+ },
+ "proposedEnrollment": {
+ "type": "number",
+ "description": "Duration of enrollment from the start date in days"
+ },
+ "referenceBranch": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The slug of the reference branch"
+ },
+ "filter_expression": {
+ "type": "string",
+ "description": "This is NOT used by Nimbus, but has special functionality in Remote Settings.\nSee https://remote-settings.readthedocs.io/en/latest/target-filters.html#how"
+ }
+ },
+ "required": [
+ "slug",
+ "id",
+ "application",
+ "userFacingName",
+ "userFacingDescription",
+ "isEnrollmentPaused",
+ "bucketConfig",
+ "probeSets",
+ "branches",
+ "startDate",
+ "endDate",
+ "proposedEnrollment",
+ "referenceBranch"
+ ],
+ "additionalProperties": true,
+ "description": "The experiment definition accessible to:\n1. The Nimbus SDK via Remote Settings\n2. Jetstream via the Experimenter API"
+ }
+ }
+}
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/SpecialMessageActionSchemas.json b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/SpecialMessageActionSchemas.json
new file mode 100644
index 0000000000..f7fe9c9406
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/SpecialMessageActionSchemas.json
@@ -0,0 +1,396 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$ref": "#/definitions/SpecialMessageActionSchemas",
+ "definitions": {
+ "SpecialMessageActionSchemas": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["DISABLE_STP_DOORHANGERS"]
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": false,
+ "description": "Disables all STP doorhangers."
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object",
+ "properties": {
+ "args": {
+ "type": "string",
+ "description": "The element to highlight"
+ }
+ },
+ "required": ["args"],
+ "additionalProperties": false
+ },
+ "type": {
+ "type": "string",
+ "enum": ["HIGHLIGHT_FEATURE"]
+ }
+ },
+ "required": ["data", "type"],
+ "additionalProperties": false,
+ "description": "Highlights an element, such as a menu item"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object",
+ "properties": {
+ "telemetrySource": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ }
+ },
+ "required": ["telemetrySource", "url"],
+ "additionalProperties": false
+ },
+ "type": {
+ "type": "string",
+ "enum": ["INSTALL_ADDON_FROM_URL"]
+ }
+ },
+ "required": ["data", "type"],
+ "additionalProperties": false,
+ "description": "Install an add-on from AMO"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object",
+ "properties": {
+ "args": {
+ "type": "string",
+ "description": "The about page. E.g. \"welcome\" for about:welcome'"
+ },
+ "where": {
+ "type": "string",
+ "enum": ["current", "save", "tab", "tabshifted", "window"],
+ "description": "Where the URL is opened",
+ "default": "tab"
+ },
+ "entrypoint": {
+ "type": "string",
+ "description": "Any optional entrypoint value that will be added to the search. E.g. \"foo=bar\" would result in about:welcome?foo=bar'"
+ }
+ },
+ "required": ["args", "where", "entrypoint"],
+ "additionalProperties": false
+ },
+ "type": {
+ "type": "string",
+ "enum": ["OPEN_ABOUT_PAGE"]
+ }
+ },
+ "required": ["data", "type"],
+ "additionalProperties": false,
+ "description": "Opens an about: page in Firefox"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object",
+ "properties": {
+ "args": {
+ "type": "string",
+ "description": "The menu name, e.g. \"appMenu\""
+ }
+ },
+ "required": ["args"],
+ "additionalProperties": false
+ },
+ "type": {
+ "type": "string",
+ "enum": ["OPEN_APPLICATIONS_MENU"]
+ }
+ },
+ "required": ["data", "type"],
+ "additionalProperties": false,
+ "description": "Opens an application menu"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["OPEN_AWESOME_BAR"]
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": false,
+ "description": "Focuses and expands the awesome bar"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object",
+ "properties": {
+ "category": {
+ "type": "string",
+ "description": "Section of about:preferences, e.g. \"privacy-reports\""
+ },
+ "entrypoint": {
+ "type": "string",
+ "description": "Add a queryparam for metrics"
+ }
+ },
+ "required": ["category"],
+ "additionalProperties": false
+ },
+ "type": {
+ "type": "string",
+ "enum": ["OPEN_PREFERENCES_PAGE"]
+ }
+ },
+ "required": ["data", "type"],
+ "additionalProperties": false,
+ "description": "Opens a preference page"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["OPEN_PRIVATE_BROWSER_WINDOW"]
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": false,
+ "description": "Opens a private browsing window."
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["OPEN_PROTECTION_PANEL"]
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": false,
+ "description": "Opens the protections panel"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["OPEN_PROTECTION_REPORT"]
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": false,
+ "description": "Opens the protections panel report"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object",
+ "properties": {
+ "args": {
+ "type": "string",
+ "description": "URL to open"
+ },
+ "where": {
+ "type": "string",
+ "enum": ["current", "save", "tab", "tabshifted", "window"],
+ "description": "Where the URL is opened",
+ "default": "tab"
+ }
+ },
+ "required": ["args", "where"],
+ "additionalProperties": false
+ },
+ "type": {
+ "type": "string",
+ "enum": ["OPEN_URL"]
+ }
+ },
+ "required": ["data", "type"],
+ "additionalProperties": false,
+ "description": "Opens given URL"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["PIN_CURRENT_TAB"]
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": false,
+ "description": "Pin the current tab"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["SHOW_FIREFOX_ACCOUNTS"]
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "entrypoint": {
+ "type": "string",
+ "description": "Adds entrypoint={your value} to the FXA URL"
+ },
+ "extraParams": {
+ "type": "object",
+ "description": "Any extra parameter that will be added to the FXA URL. E.g. {foo: bar} would result in <FXA_url>?foo=bar'"
+ }
+ },
+ "required": ["entrypoint"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["type", "data"],
+ "additionalProperties": false,
+ "description": "Show Firefox Accounts"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["SHOW_MIGRATION_WIZARD"]
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "source": {
+ "type": "string",
+ "description": "Identitifer of the browser that should be pre-selected in the import migration wizard popup (e.g. 'chrome'), See https://searchfox.org/mozilla-central/rev/8dae1cc76a6b45e05198bc0d5d4edb7bf1003265/browser/components/migration/MigrationUtils.jsm#917"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": false,
+ "description": "Shows the Migration Wizard to import data from another Browser. See https://support.mozilla.org/en-US/kb/import-data-another-browser\""
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["CANCEL"]
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": false,
+ "description": "Minimize the CFR doorhanger back into the URLbar"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["ACCEPT_DOH"]
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": false,
+ "description": "Accept DOH doorhanger notification"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["DISABLE_DOH"]
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": false,
+ "description": "Dismiss DOH doorhanger notification"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": ["SET_DEFAULT_BROWSER"]
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": false,
+ "description": "Message action to set Firefox as default browser"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object",
+ "properties": {
+ "homePage": {
+ "type": "string",
+ "description": "Should reset homepage pref",
+ "enum": ["default"]
+ },
+ "newtab": {
+ "type": "string",
+ "enum": ["default"],
+ "description": "Should reset newtab pref"
+ },
+ "layout": {
+ "type": "object",
+ "description": "Section name and boolean value that specifies if the section should be on or off.",
+ "properties": {
+ "search": {
+ "type": "boolean"
+ },
+ "topsites": {
+ "type": "boolean"
+ },
+ "highlights": {
+ "type": "boolean"
+ },
+ "snippets": {
+ "type": "boolean"
+ },
+ "topstories": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "search",
+ "topsites",
+ "highlights",
+ "snippets",
+ "topstories"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "type": {
+ "type": "string",
+ "enum": ["CONFIGURE_HOMEPAGE"]
+ }
+ },
+ "required": ["data", "type"],
+ "additionalProperties": false,
+ "description": "Resets homepage pref and sections layout"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/index.md b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/index.md
new file mode 100644
index 0000000000..b4af53ba67
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/index.md
@@ -0,0 +1,243 @@
+# User Actions
+
+A subset of actions are available to messages via fields like `button_action` for snippets, or `primary_action` for CFRs.
+
+## Usage
+
+For snippets, you should add the action type in `button_action` and any additional parameters in `button_action_args. For example:
+
+```json
+{
+ "button_action": "OPEN_ABOUT_PAGE",
+ "button_action_args": "config"
+}
+```
+
+## How to update
+
+Make a pull request against [mozilla/nimbus-shared](https://github.com/mozilla/nimbus-shared/) repo with your changes.
+Build and copy over resulting schema from `nimbus-shared/schemas/messaging/` to `toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas.json`.
+
+## Available Actions
+
+### `OPEN_APPLICATIONS_MENU`
+
+* args: (none)
+
+Opens the applications menu.
+
+### `OPEN_PRIVATE_BROWSER_WINDOW`
+
+* args: (none)
+
+Opens a new private browsing window.
+
+
+### `OPEN_URL`
+
+* args: `string` (a url)
+
+Opens a given url.
+
+Example:
+
+```json
+{
+ "button_action": "OPEN_URL",
+ "button_action_args": "https://foo.com"
+}
+```
+
+### `OPEN_ABOUT_PAGE`
+
+* args:
+```ts
+{
+ args: string, // (a valid about page without the `about:` prefix)
+ entrypoint?: string, // URL search param used for referrals
+}
+```
+
+Opens a given about page
+
+Example:
+
+```json
+{
+ "button_action": "OPEN_ABOUT_PAGE",
+ "button_action_args": "config"
+}
+```
+
+### `OPEN_PREFERENCES_PAGE`
+
+* args:
+```
+{
+ args?: string, // (a category accessible via a `#`)
+ entrypoint?: string // URL search param used to referrals
+
+Opens `about:preferences` with an optional category accessible via a `#` in the URL (e.g. `about:preferences#home`).
+
+Example:
+
+```json
+{
+ "button_action": "OPEN_PREFERENCES_PAGE",
+ "button_action_args": "home"
+}
+```
+
+### `SHOW_FIREFOX_ACCOUNTS`
+
+* args: (none)
+
+Opens Firefox accounts sign-up page. Encodes some information that the origin was from snippets by default.
+
+### `SHOW_MIGRATION_WIZARD`
+
+* args: (none)
+
+Opens import wizard to bring in settings and data from another browser.
+
+### `PIN_CURRENT_TAB`
+
+* args: (none)
+
+Pins the currently focused tab.
+
+### `ENABLE_FIREFOX_MONITOR`
+
+* args:
+```ts
+{
+ url: string;
+ flowRequestParams: {
+ entrypoint: string;
+ utm_term: string;
+ form_type: string;
+ }
+}
+```
+
+Opens an oauth flow to enable Firefox Monitor at a given `url` and adds Firefox metrics that user given a set of `flowRequestParams`.
+
+#### `url`
+
+The URL should start with `https://monitor.firefox.com/oauth/init` and add various metrics tags as search params, including:
+
+* `utm_source`
+* `utm_campaign`
+* `form_type`
+* `entrypoint`
+
+You should verify the values of these search params with whoever is doing the data analysis (e.g. Leif Oines).
+
+#### `flowRequestParams`
+
+These params are used by Firefox to add information specific to that individual user to the final oauth URL. You should include:
+
+* `entrypoint`
+* `utm_term`
+* `form_type`
+
+The `entrypoint` and `form_type` values should match the encoded values in your `url`.
+
+You should verify the values with whoever is doing the data analysis (e.g. Leif Oines).
+
+#### Example
+
+```json
+{
+ "button_action": "ENABLE_FIREFOX_MONITOR",
+ "button_action_args": {
+ "url": "https://monitor.firefox.com/oauth/init?utm_source=snippets&utm_campaign=monitor-snippet-test&form_type=email&entrypoint=newtab",
+ "flowRequestParams": {
+ "entrypoint": "snippets",
+ "utm_term": "monitor",
+ "form_type": "email"
+ }
+ }
+}
+```
+
+### `HIGHLIGHT_FEATURE`
+
+Can be used to highlight (show a light blue overlay) a certain button or part of the browser UI.
+
+* args: `string` a [valid targeting defined in the UITour](https://searchfox.org/mozilla-central/rev/7fd1c1c34923ece7ad8c822bee062dd0491d64dc/browser/components/uitour/UITour.jsm#108)
+
+### `INSTALL_ADDON_FROM_URL`
+
+Can be used to install an addon from addons.mozilla.org.
+
+* args:
+```ts
+{
+ url: string,
+ telemetrySource?: string
+};
+```
+
+### `OPEN_PROTECTION_REPORT`
+
+Opens `about:protections`
+
+### `OPEN_PROTECTION_PANEL`
+
+Opens the protection panel behind on the lock icon of the awesomebar
+
+### `DISABLE_STP_DOORHANGERS`
+
+Disables all Social Tracking Protection messages
+
+* args: (none)
+
+### `OPEN_AWESOME_BAR`
+
+Focuses and expands the awesome bar.
+
+* args: (none)
+
+### `CANCEL`
+
+No-op action used to dismiss CFR notifications (but not remove or block them)
+
+* args: (none)
+
+### `DISABLE_DOH`
+
+User action for turning off the DoH feature
+
+* args: (none)
+
+### `ACCEPT_DOH`
+
+User action for continuing to use the DoH feature
+
+* args: (none)
+
+### `CONFIGURE_HOMEPAGE`
+
+Action for configuring the user homepage and restoring defaults.
+
+* args:
+```ts
+{
+ homePage: "default" | null;
+ newtab: "default" | null;
+ layout: {
+ search: boolean;
+ topsites: boolean;
+ highlights: boolean;
+ topstories: boolean;
+ snippets: boolean;
+ }
+}
+```
+
+### `SET_DEFAULT_BROWSER`
+
+Action for configuring the default browser to Firefox on the user's system.
+
+* args: (none)
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser.ini b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser.ini
new file mode 100644
index 0000000000..32bcff6a4d
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+prefs =
+ identity.fxaccounts.remote.root=https://example.com/
+support-files =
+ head.js
+ ../../index.md
+
+[browser_sma_open_about_page.js]
+[browser_sma_open_awesome_bar.js]
+[browser_sma_open_private_browser_window.js]
+[browser_sma_open_protection_panel.js]
+[browser_sma_open_protection_report.js]
+[browser_sma_open_url.js]
+[browser_sma_pin_current_tab.js]
+[browser_sma_show_firefox_accounts.js]
+[browser_sma_show_migration_wizard.js]
+[browser_sma.js]
+[browser_sma_docs.js]
+[browser_sma_accept_doh.js]
+[browser_sma_disable_doh.js]
+[browser_sma_cfrmessageprovider.js]
+[browser_sma_configure_homepage.js]
+[browser_sma_default_browser.js]
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma.js
new file mode 100644
index 0000000000..b46b3730e9
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_unknown_event() {
+ let error;
+ try {
+ await SpecialMessageActions.handleAction(
+ { type: "UNKNOWN_EVENT_123" },
+ gBrowser
+ );
+ } catch (e) {
+ error = e;
+ }
+ ok(error, "should throw if an unexpected event is handled");
+ Assert.equal(
+ error.message,
+ "Special message action with type UNKNOWN_EVENT_123 is unsupported."
+ );
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_accept_doh.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_accept_doh.js
new file mode 100644
index 0000000000..f9255b17ec
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_accept_doh.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const DOH_DOORHANGER_DECISION_PREF = "doh-rollout.doorhanger-decision";
+
+add_task(async function test_disable_doh() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[DOH_DOORHANGER_DECISION_PREF, ""]],
+ });
+ await SMATestUtils.executeAndValidateAction({ type: "ACCEPT_DOH" });
+ Assert.equal(
+ Services.prefs.getStringPref(DOH_DOORHANGER_DECISION_PREF, ""),
+ "UIOk",
+ "Pref should be set on accept"
+ );
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_cancel.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_cancel.js
new file mode 100644
index 0000000000..ca42dac563
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_cancel.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_cancel_event() {
+ let error = null;
+ try {
+ await SMATestUtils.executeAndValidateAction({ type: "CANCEL" });
+ } catch (e) {
+ error = e;
+ }
+ ok(!error, "should not throw for CANCEL");
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_cfrmessageprovider.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_cfrmessageprovider.js
new file mode 100644
index 0000000000..93f3cc851f
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_cfrmessageprovider.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { CFRMessageProvider } = ChromeUtils.import(
+ "resource://activity-stream/lib/CFRMessageProvider.jsm"
+);
+
+add_task(async function test_all_test_messages() {
+ let messagesWithButtons = (await CFRMessageProvider.getMessages()).filter(
+ m => m.content.buttons
+ );
+
+ for (let message of messagesWithButtons) {
+ info(`Testing ${message.id}`);
+ if (message.template === "infobar") {
+ for (let button of message.content.buttons) {
+ await SMATestUtils.validateAction(button.action);
+ }
+ } else {
+ let { primary, secondary } = message.content.buttons;
+ await SMATestUtils.validateAction(primary.action);
+ for (let secondaryBtn of secondary) {
+ if (secondaryBtn.action) {
+ await SMATestUtils.validateAction(secondaryBtn.action);
+ }
+ }
+ }
+ }
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_configure_homepage.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_configure_homepage.js
new file mode 100644
index 0000000000..8bc734cece
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_configure_homepage.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PromiseUtils } = ChromeUtils.import(
+ "resource://gre/modules/PromiseUtils.jsm"
+);
+
+const HOMEPAGE_PREF = "browser.startup.homepage";
+const NEWTAB_PREF = "browser.newtabpage.enabled";
+const HIGHLIGHTS_PREF =
+ "browser.newtabpage.activity-stream.feeds.section.highlights";
+const HIGHLIGHTS_ROWS_PREF =
+ "browser.newtabpage.activity-stream.section.highlights.rows";
+const SEARCH_PREF = "browser.newtabpage.activity-stream.showSearch";
+const TOPSITES_PREF = "browser.newtabpage.activity-stream.feeds.topsites";
+const SNIPPETS_PREF = "browser.newtabpage.activity-stream.feeds.snippets";
+const TOPSTORIES_PREF =
+ "browser.newtabpage.activity-stream.feeds.system.topstories";
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ // Highlights are preffed off by default.
+ set: [[HIGHLIGHTS_PREF, true]],
+ });
+
+ registerCleanupFunction(async () => {
+ await SpecialPowers.popPrefEnv();
+ [
+ HOMEPAGE_PREF,
+ NEWTAB_PREF,
+ HIGHLIGHTS_PREF,
+ HIGHLIGHTS_ROWS_PREF,
+ SEARCH_PREF,
+ TOPSITES_PREF,
+ SNIPPETS_PREF,
+ ].forEach(prefName => Services.prefs.clearUserPref(prefName));
+ });
+});
+
+function waitForHomeNavigation() {
+ let deferred = PromiseUtils.defer();
+ let navigation = {
+ observe(subject) {
+ if (subject === "browser-open-homepage-start") {
+ deferred.resolve();
+ Services.obs.removeObserver(navigation, "browser-open-homepage-start");
+ }
+ },
+ };
+ Services.obs.addObserver(navigation, "browser-open-homepage-start");
+
+ return deferred;
+}
+
+add_task(async function test_CONFIGURE_HOMEPAGE_newtab_home_prefs() {
+ const action = {
+ type: "CONFIGURE_HOMEPAGE",
+ data: { homePage: "default", newtab: "default" },
+ };
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [HOMEPAGE_PREF, "about:blank"],
+ [NEWTAB_PREF, false],
+ ],
+ });
+
+ Assert.ok(Services.prefs.prefHasUserValue(HOMEPAGE_PREF), "Test setup ok");
+ Assert.ok(Services.prefs.prefHasUserValue(NEWTAB_PREF), "Test setup ok");
+
+ await SMATestUtils.executeAndValidateAction(action);
+
+ Assert.ok(
+ !Services.prefs.prefHasUserValue(HOMEPAGE_PREF),
+ "Homepage pref should be back to default"
+ );
+ Assert.ok(
+ !Services.prefs.prefHasUserValue(NEWTAB_PREF),
+ "Newtab pref should be back to default"
+ );
+});
+
+add_task(async function test_CONFIGURE_HOMEPAGE_layout_prefs() {
+ const action = {
+ type: "CONFIGURE_HOMEPAGE",
+ data: {
+ layout: {
+ search: true,
+ topsites: false,
+ highlights: false,
+ snippets: false,
+ topstories: false,
+ },
+ },
+ };
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [HIGHLIGHTS_ROWS_PREF, 3],
+ [SEARCH_PREF, false],
+ ],
+ });
+
+ await SMATestUtils.executeAndValidateAction(action);
+
+ Assert.ok(Services.prefs.getBoolPref(SEARCH_PREF), "Search is turned on");
+ Assert.ok(
+ !Services.prefs.getBoolPref(TOPSITES_PREF),
+ "Topsites are turned off"
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref(HIGHLIGHTS_PREF),
+ "HIGHLIGHTS_PREF are on because they have been customized"
+ );
+ Assert.ok(
+ !Services.prefs.getBoolPref(TOPSTORIES_PREF),
+ "Topstories are turned off"
+ );
+ Assert.ok(
+ !Services.prefs.getBoolPref(SNIPPETS_PREF),
+ "Snippets are turned off"
+ );
+});
+
+add_task(async function test_CONFIGURE_HOMEPAGE_home_redirect() {
+ const action = {
+ type: "CONFIGURE_HOMEPAGE",
+ data: { homePage: "default", newtab: "default" },
+ };
+
+ let browser = gBrowser.selectedBrowser;
+ // Wait for any other navigation events from previous tests
+ await BrowserTestUtils.browserLoaded(browser, false, "about:home");
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:config");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:config");
+
+ await SMATestUtils.executeAndValidateAction(action);
+
+ await waitForHomeNavigation();
+ Assert.ok(true, "Redirected to about:home");
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_default_browser.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_default_browser.js
new file mode 100644
index 0000000000..2d919456cd
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_default_browser.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_set_default_browser() {
+ const sandbox = sinon.createSandbox();
+ const stub = sandbox.stub();
+
+ await SMATestUtils.executeAndValidateAction(
+ { type: "SET_DEFAULT_BROWSER" },
+ {
+ ownerGlobal: {
+ getShellService: () => ({
+ setAsDefault: stub,
+ }),
+ },
+ }
+ );
+
+ Assert.equal(stub.callCount, 1, "setAsDefault was called by the action");
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_disable_doh.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_disable_doh.js
new file mode 100644
index 0000000000..aa61214360
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_disable_doh.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const DOH_DOORHANGER_DECISION_PREF = "doh-rollout.doorhanger-decision";
+const NETWORK_TRR_MODE_PREF = "network.trr.mode";
+
+add_task(async function test_disable_doh() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [DOH_DOORHANGER_DECISION_PREF, "mochitest"],
+ [NETWORK_TRR_MODE_PREF, 0],
+ ],
+ });
+
+ await SMATestUtils.executeAndValidateAction({ type: "DISABLE_DOH" });
+
+ Assert.equal(
+ Services.prefs.getStringPref(DOH_DOORHANGER_DECISION_PREF, ""),
+ "UIDisabled",
+ "Pref should be set on disabled"
+ );
+ Assert.equal(
+ Services.prefs.getIntPref(NETWORK_TRR_MODE_PREF, 0),
+ 5,
+ "Pref should be set on disabled"
+ );
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_docs.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_docs.js
new file mode 100644
index 0000000000..d67843db74
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_docs.js
@@ -0,0 +1,30 @@
+const TEST_URL =
+ "https://example.com/browser/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/index.md";
+
+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_sma_docs() {
+ let request = await fetch(TEST_URL);
+ let docs = await request.text();
+ let headings = getHeadingsFromDocs(docs);
+ const schemaTypes = (await fetchSMASchema).anyOf.map(
+ s => s.properties.type.enum[0]
+ );
+ for (let schemaType of schemaTypes) {
+ Assert.ok(
+ headings.includes(schemaType),
+ `${schemaType} not found in SpecialMessageActionSchemas/index.md`
+ );
+ }
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_about_page.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_about_page.js
new file mode 100644
index 0000000000..264646bd0e
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_about_page.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_OPEN_ABOUT_PAGE() {
+ const tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:logins?foo=bar"
+ );
+ await SMATestUtils.executeAndValidateAction({
+ type: "OPEN_ABOUT_PAGE",
+ data: { args: "logins", entrypoint: "foo=bar", where: "tabshifted" },
+ });
+
+ const tab = await tabPromise;
+ ok(tab, "should open about page with entrypoint in a new tab by default");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_OPEN_ABOUT_PAGE_NEW_WINDOW() {
+ const newWindowPromise = BrowserTestUtils.waitForNewWindow(
+ gBrowser,
+ "about:robots?foo=bar"
+ );
+ await SMATestUtils.executeAndValidateAction({
+ type: "OPEN_ABOUT_PAGE",
+ data: { args: "robots", entrypoint: "foo=bar", where: "window" },
+ });
+
+ const win = await newWindowPromise;
+ ok(win, "should open about page in a new window");
+ BrowserTestUtils.closeWindow(win);
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_awesome_bar.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_awesome_bar.js
new file mode 100644
index 0000000000..62f7d8bb68
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_awesome_bar.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_OPEN_AWESOME_BAR() {
+ await SMATestUtils.executeAndValidateAction({ type: "OPEN_AWESOME_BAR" });
+ Assert.ok(gURLBar.focused, "Focus should be on awesome bar");
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_private_browser_window.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_private_browser_window.js
new file mode 100644
index 0000000000..b6c933fbcf
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_private_browser_window.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_OPEN_PRIVATE_BROWSER_WINDOW() {
+ const newWindowPromise = BrowserTestUtils.waitForNewWindow();
+ await SMATestUtils.executeAndValidateAction({
+ type: "OPEN_PRIVATE_BROWSER_WINDOW",
+ });
+ const win = await newWindowPromise;
+ ok(
+ PrivateBrowsingUtils.isWindowPrivate(win),
+ "should open a private browsing window"
+ );
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_protection_panel.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_protection_panel.js
new file mode 100644
index 0000000000..c9522426a2
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_protection_panel.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_OPEN_PROTECTION_PANEL() {
+ await BrowserTestUtils.withNewTab(EXAMPLE_URL, async browser => {
+ const popupshown = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ e => e.target.id == "protections-popup"
+ );
+
+ await SMATestUtils.executeAndValidateAction({
+ type: "OPEN_PROTECTION_PANEL",
+ });
+
+ let { target: popupEl } = await popupshown;
+ Assert.equal(popupEl.state, "open", "Protections popup is open.");
+ });
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_protection_report.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_protection_report.js
new file mode 100644
index 0000000000..f9d4fa1252
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_protection_report.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_OPEN_PROTECTION_REPORT() {
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ let loaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ "about:protections"
+ );
+
+ await SMATestUtils.executeAndValidateAction({
+ type: "OPEN_PROTECTION_REPORT",
+ });
+
+ await loaded;
+
+ // When the graph is built it means any messaging has finished,
+ // we can close the tab.
+ await SpecialPowers.spawn(browser, [], async function() {
+ await ContentTaskUtils.waitForCondition(() => {
+ let bars = content.document.querySelectorAll(".graph-bar");
+ return bars.length;
+ }, "The graph has been built");
+ });
+ });
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_url.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_url.js
new file mode 100644
index 0000000000..876193b7ad
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_open_url.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_OPEN_URL() {
+ const action = {
+ type: "OPEN_URL",
+ data: { args: EXAMPLE_URL, where: "current" },
+ };
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ const loaded = BrowserTestUtils.browserLoaded(browser);
+ await SMATestUtils.executeAndValidateAction(action);
+ const url = await loaded;
+ Assert.equal(
+ url,
+ "https://example.com/",
+ "should open URL in the same tab"
+ );
+ });
+});
+
+add_task(async function test_OPEN_URL_new_tab() {
+ const action = {
+ type: "OPEN_URL",
+ data: { args: EXAMPLE_URL, where: "tab" },
+ };
+ const tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, EXAMPLE_URL);
+ await SpecialMessageActions.handleAction(action, gBrowser);
+ const browser = await tabPromise;
+ ok(browser, "should open URL in a new tab");
+ BrowserTestUtils.removeTab(browser);
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_pin_current_tab.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_pin_current_tab.js
new file mode 100644
index 0000000000..4425325526
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_pin_current_tab.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_PIN_CURRENT_TAB() {
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ await SMATestUtils.executeAndValidateAction({ type: "PIN_CURRENT_TAB" });
+
+ ok(gBrowser.selectedTab.pinned, "should pin current tab");
+
+ gBrowser.unpinTab(gBrowser.selectedTab);
+ });
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_show_firefox_accounts.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_show_firefox_accounts.js
new file mode 100644
index 0000000000..3f99fa77ef
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_show_firefox_accounts.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Note: "identity.fxaccounts.remote.root" is set to https://example.com in browser.ini
+add_task(async function test_SHOW_FIREFOX_ACCOUNTS() {
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ let loaded = BrowserTestUtils.browserLoaded(browser);
+ await SMATestUtils.executeAndValidateAction({
+ type: "SHOW_FIREFOX_ACCOUNTS",
+ data: { entrypoint: "snippets" },
+ });
+ Assert.equal(
+ await loaded,
+ "https://example.com/?context=fx_desktop_v3&entrypoint=snippets&action=email&service=sync",
+ "should load fxa with endpoint=snippets"
+ );
+
+ // Open a URL
+ loaded = BrowserTestUtils.browserLoaded(browser);
+ await SMATestUtils.executeAndValidateAction({
+ type: "SHOW_FIREFOX_ACCOUNTS",
+ data: { entrypoint: "aboutwelcome" },
+ });
+
+ Assert.equal(
+ await loaded,
+ "https://example.com/?context=fx_desktop_v3&entrypoint=aboutwelcome&action=email&service=sync",
+ "should load fxa with a custom endpoint"
+ );
+
+ // Open a URL with extra parameters
+ loaded = BrowserTestUtils.browserLoaded(browser);
+ await SMATestUtils.executeAndValidateAction({
+ type: "SHOW_FIREFOX_ACCOUNTS",
+ data: { entrypoint: "test", extraParams: { foo: "bar" } },
+ });
+
+ Assert.equal(
+ await loaded,
+ "https://example.com/?context=fx_desktop_v3&entrypoint=test&action=email&service=sync&foo=bar",
+ "should load fxa with a custom endpoint and extra parameters in url"
+ );
+ });
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_show_migration_wizard.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_show_migration_wizard.js
new file mode 100644
index 0000000000..e9123c0b36
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_show_migration_wizard.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { MigrationUtils } = ChromeUtils.import(
+ "resource:///modules/MigrationUtils.jsm"
+);
+
+add_task(async function test_SHOW_MIGRATION_WIZARD() {
+ let migratorOpen = TestUtils.waitForCondition(() => {
+ let win = Services.wm.getMostRecentWindow("Browser:MigrationWizard");
+ return win && win.document && win.document.readyState == "complete";
+ }, "Migrator window loaded");
+
+ // We can't call this code directly or our JS execution will get blocked on Windows/Linux where
+ // the dialog is modal.
+ executeSoon(() =>
+ SMATestUtils.executeAndValidateAction({ type: "SHOW_MIGRATION_WIZARD" })
+ );
+
+ await migratorOpen;
+ let migratorWindow = Services.wm.getMostRecentWindow(
+ "Browser:MigrationWizard"
+ );
+ ok(migratorWindow, "Migrator window opened");
+ await BrowserTestUtils.closeWindow(migratorWindow);
+});
+
+add_task(async function test_SHOW_MIGRATION_WIZARD_WITH_SOURCE() {
+ let migratorOpen = TestUtils.waitForCondition(() => {
+ let win = Services.wm.getMostRecentWindow("Browser:MigrationWizard");
+ return win && win.document && win.document.readyState == "complete";
+ }, "Migrator window loaded");
+
+ // We can't call this code directly or our JS execution will get blocked on Windows/Linux where
+ // the dialog is modal.
+ executeSoon(() =>
+ SMATestUtils.executeAndValidateAction({
+ type: "SHOW_MIGRATION_WIZARD",
+ data: { source: "chrome" },
+ })
+ );
+
+ await migratorOpen;
+ let migratorWindow = Services.wm.getMostRecentWindow(
+ "Browser:MigrationWizard"
+ );
+ ok(migratorWindow, "Migrator window opened when source param specified");
+ await BrowserTestUtils.closeWindow(migratorWindow);
+});
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/head.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/head.js
new file mode 100644
index 0000000000..46a11275f3
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/head.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "SpecialMessageActions",
+ "resource://messaging-system/lib/SpecialMessageActions.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "Ajv",
+ "resource://testing-common/ajv-4.1.1.js"
+);
+
+XPCOMUtils.defineLazyGetter(this, "fetchSMASchema", async () => {
+ const response = await fetch(
+ "resource://testing-common/SpecialMessageActionSchemas.json"
+ );
+ const schema = await response.json();
+ if (!schema) {
+ throw new Error("Failed to load SpecialMessageActionSchemas");
+ }
+ return schema.definitions.SpecialMessageActionSchemas;
+});
+
+const EXAMPLE_URL = "https://example.com/";
+
+const SMATestUtils = {
+ /**
+ * Checks if an action is valid acording to existing schemas
+ * @param {SpecialMessageAction} action
+ */
+ async validateAction(action) {
+ const schema = await fetchSMASchema;
+ const ajv = new Ajv({ async: "co*" });
+ const validator = ajv.compile(schema);
+ if (!validator(action)) {
+ throw new Error(`Action with type ${action.type} was not valid.`);
+ }
+ ok(!validator.errors, `should be a valid action of type ${action.type}`);
+ },
+
+ /**
+ * Executes a Special Message Action after validating it
+ * @param {SpecialMessageAction} action
+ * @param {Browser} browser
+ */
+ async executeAndValidateAction(action, browser = gBrowser) {
+ await SMATestUtils.validateAction(action);
+ await SpecialMessageActions.handleAction(action, browser);
+ },
+};
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..682603f82c
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/TriggerActionSchemas.json
@@ -0,0 +1,192 @@
+{
+ "$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": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ },
+ "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"
+ }
+ ]
+ }
+ }
+}
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..11898c4ccd
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/index.md
@@ -0,0 +1,121 @@
+# Trigger Listeners
+
+A set of action listeners that can be used to trigger CFR messages.
+
+## How to update
+
+Make a pull request against [mozilla/nimbus-shared](https://github.com/mozilla/nimbus-shared/) repo with your changes.
+Build and copy over resulting schema from `nimbus-shared/schemas/messaging/` to `toolkit/components/messaging-system/schemas/TriggerActionSchemas.json`.
+
+## 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.
+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 a flag or a combination of flags merged together by
+ANDing the various STATE_BLOCKED_* flags.
+
+```typescript
+// https://searchfox.org/mozilla-central/rev/2fcab997046ba9e068c5391dc7d8848e121d84f8/uriloader/base/nsIWebProgressListener.idl#260
+let event: ContentBlockingEventFlag;
+let pageLoad = number;
+```
+
+### `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;
+```
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..63598cbfb3
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files =
+ ../../index.md
+
+[browser_asrouter_trigger_listeners.js]
+[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..4146c001c1
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser_asrouter_trigger_docs.js
@@ -0,0 +1,66 @@
+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 { Ajv } = ChromeUtils.import("resource://testing-common/ajv-4.1.1.js");
+
+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 ajv = new Ajv({ async: "co*" });
+ const validator = ajv.compile(schema);
+ if (!validator(trigger)) {
+ throw new Error(`Trigger with id ${trigger.id} was not valid.`);
+ }
+ Assert.ok(
+ !validator.errors,
+ `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..b38f14e449
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/TriggerActionSchemas/test/browser/browser_asrouter_trigger_listeners.js
@@ -0,0 +1,507 @@
+ChromeUtils.defineModuleGetter(
+ this,
+ "ASRouterTriggerListeners",
+ "resource://activity-stream/lib/ASRouterTriggerListeners.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "TestUtils",
+ "resource://testing-common/TestUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm"
+);
+
+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");
+});
diff --git a/toolkit/components/messaging-system/schemas/index.rst b/toolkit/components/messaging-system/schemas/index.rst
new file mode 100644
index 0000000000..952e9756d0
--- /dev/null
+++ b/toolkit/components/messaging-system/schemas/index.rst
@@ -0,0 +1,18 @@
+Messaging System Schemas
+========================
+
+Docs
+----
+
+More information about `Messaging System`__.
+
+.. __: /browser/components/newtab/content-src/asrouter/docs
+
+Triggers and actions
+---------------------
+
+.. toctree::
+ :maxdepth: 2
+
+ SpecialMessageActionSchemas/index
+ TriggerActionSchemas/index