diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/messaging-system/schemas | |
parent | Initial commit. (diff) | |
download | firefox-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')
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 |