mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 05:11:16 +00:00
Bug 1771073 - Correct FxMS features and keep them up to date with tests r=dmose,pdahiya
The Nimbus Features corresponding to FxMS messaging surfaces are actually intended to map to FxMS message groups, which can accept *any* FxMS message. The features have been updated with schemas that accept any FxMS message. As part of this, all FxMS schemas have been updated with an `$id` so that they can be bundled into feature schemas and have their internal `$ref`s work. (Otherwise, a `$ref` would be relative to the top-level schema instead of the sub-schema). Schemas for individual message types are no longer exposed as resource:// URIs, except in tests, as indivual schemas are no longer required at runtime. Additionally, each FxMS schema has had its `template` field become required and requires a constant value for that schema (e.g., Spotlight requires a template value of "spotlight"). A test has been added to ensure that if any of the messaging surfaces schemas change that the feature schemas are also updated. The feature schemas can be regenerated via: ``` cd ./browser/components/newtab/content-src/asrouter/schemas ../../../../../../mach make-schemas.py ``` Differential Revision: https://phabricator.services.mozilla.com/D147332
This commit is contained in:
parent
8b624ca222
commit
edfd11a38c
@ -1510,7 +1510,7 @@ pref("browser.newtabpage.activity-stream.asrouter.providers.message-groups", "{\
|
||||
// this page over http opens us up to a man-in-the-middle attack that we'd rather not face. If you are a downstream
|
||||
// repackager of this code using an alternate snippet url, please keep your users safe
|
||||
pref("browser.newtabpage.activity-stream.asrouter.providers.snippets", "{\"id\":\"snippets\",\"enabled\":false,\"type\":\"remote\",\"url\":\"https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/\",\"updateCycleInMs\":14400000}");
|
||||
pref("browser.newtabpage.activity-stream.asrouter.providers.messaging-experiments", "{\"id\":\"messaging-experiments\",\"enabled\":true,\"type\":\"remote-experiments\",\"messageGroups\":[\"cfr\",\"aboutwelcome\",\"infobar\",\"spotlight\",\"moments-page\",\"pbNewtab\"],\"updateCycleInMs\":3600000}");
|
||||
pref("browser.newtabpage.activity-stream.asrouter.providers.messaging-experiments", "{\"id\":\"messaging-experiments\",\"enabled\":true,\"type\":\"remote-experiments\",\"updateCycleInMs\":3600000}");
|
||||
|
||||
// ASRouter user prefs
|
||||
pref("browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons", true);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import json
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
|
||||
SCHEMA_BASE_DIR = Path("..", "templates")
|
||||
|
||||
|
||||
MESSAGE_TYPES = {
|
||||
"CFRUrlbarChiclet": Path("CFR", "templates", "CFRUrlbarChiclet.schema.json"),
|
||||
"ExtensionDoorhanger": Path("CFR", "templates", "ExtensionDoorhanger.schema.json"),
|
||||
"InfoBar": Path("CFR", "templates", "InfoBar.schema.json"),
|
||||
"NewtabPromoMessage": Path("PBNewtab", "NewtabPromoMessage.schema.json"),
|
||||
"Spotlight": Path("OnboardingMessage", "Spotlight.schema.json"),
|
||||
"ToolbarBadgeMessage": Path("OnboardingMessage", "ToolbarBadgeMessage.schema.json"),
|
||||
"UpdateAction": Path("OnboardingMessage", "UpdateAction.schema.json"),
|
||||
"WhatsNewMessage": Path("OnboardingMessage", "WhatsNewMessage.schema.json"),
|
||||
}
|
||||
|
||||
|
||||
def read_schema(path):
|
||||
"""Read a schema from disk and parse it as JSON."""
|
||||
with path.open("r") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def extract_template_values(template):
|
||||
"""Extract the possible template values (either via JSON Schema enum or const)."""
|
||||
enum = template.get("enum")
|
||||
if enum:
|
||||
return enum
|
||||
|
||||
const = template.get("const")
|
||||
if const:
|
||||
return [const]
|
||||
|
||||
|
||||
def main(check=False):
|
||||
"""Generate Nimbus feature schemas for Firefox Messaging System."""
|
||||
defs = {
|
||||
name: read_schema(SCHEMA_BASE_DIR / path)
|
||||
for name, path in MESSAGE_TYPES.items()
|
||||
}
|
||||
|
||||
# Ensure all bundled schemas have an $id so that $refs inside the
|
||||
# bundled schema work correctly (i.e, they will reference the subschema
|
||||
# and not the bundle).
|
||||
for name, schema in defs.items():
|
||||
if "$id" not in schema:
|
||||
raise ValueError(f"Schema {name} is missing an $id")
|
||||
|
||||
props = schema["properties"]
|
||||
if "template" not in props:
|
||||
raise ValueError(f"Schema {name} is missing a template")
|
||||
|
||||
template = props["template"]
|
||||
if "enum" not in template and "const" not in template:
|
||||
raise ValueError(f"Schema {name} should have const or enum template")
|
||||
|
||||
filename = Path("MessagingExperiment.schema.json")
|
||||
|
||||
templates = {
|
||||
name: extract_template_values(schema["properties"]["template"])
|
||||
for name, schema in defs.items()
|
||||
}
|
||||
|
||||
# Ensure that each schema has a unique set of template values.
|
||||
for a in templates.keys():
|
||||
a_keys = set(templates[a])
|
||||
|
||||
for b in templates.keys():
|
||||
if a == b:
|
||||
continue
|
||||
|
||||
b_keys = set(templates[b])
|
||||
intersection = a_keys.intersection(b_keys)
|
||||
|
||||
if len(intersection):
|
||||
raise ValueError(
|
||||
f"Schema {a} and {b} have overlapping template values: {', '.join(intersection)}"
|
||||
)
|
||||
|
||||
all_templates = list(chain.from_iterable(templates.values()))
|
||||
|
||||
feature_schema = {
|
||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||
"$id": "resource://activity-stream/schemas/MessagingExperiment.schema.json",
|
||||
"title": "Messaging Experiment",
|
||||
"description": "A Firefox Messaging System message.",
|
||||
"allOf": [
|
||||
# Enforce that one of the templates must match (so that one of the
|
||||
# if branches will match).
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"template": {
|
||||
"type": "string",
|
||||
"enum": all_templates,
|
||||
},
|
||||
},
|
||||
"required": ["template"],
|
||||
},
|
||||
*(
|
||||
{
|
||||
"if": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"template": {
|
||||
"type": "string",
|
||||
"enum": templates[message_type],
|
||||
},
|
||||
},
|
||||
"required": ["template"],
|
||||
},
|
||||
"then": {"$ref": f"#/$defs/{message_type}"},
|
||||
}
|
||||
for message_type in defs
|
||||
),
|
||||
],
|
||||
"$defs": defs,
|
||||
}
|
||||
|
||||
if check:
|
||||
print(f"Checking {filename} ...")
|
||||
|
||||
with filename.open("r") as f:
|
||||
on_disk = json.load(f)
|
||||
|
||||
if on_disk != feature_schema:
|
||||
print(f"{filename} does not match generated schema")
|
||||
print("Generated schema:")
|
||||
json.dump(feature_schema, sys.stdout)
|
||||
print("\n\nCommitted schema:")
|
||||
json.dump(on_disk, sys.stdout)
|
||||
|
||||
raise ValueError("Schemas do not match!")
|
||||
|
||||
else:
|
||||
with filename.open("wb") as f:
|
||||
print(f"Generating {filename} ...")
|
||||
f.write(json.dumps(feature_schema, indent=2).encode("utf-8"))
|
||||
f.write(b"\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = ArgumentParser(description=main.__doc__)
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="Check that the generated schemas have not changed",
|
||||
default=False,
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.check)
|
@ -1,4 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||
"$id": "file:///CFRUrlbarChiclet.schema.json",
|
||||
"title": "CFRUrlbarChiclet",
|
||||
"description": "A template with a chiclet button with text.",
|
||||
"version": "1.0.0",
|
||||
@ -102,7 +104,8 @@
|
||||
"description": "A JEXL expression representing targeting information"
|
||||
},
|
||||
"template": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"const": "cfr_urlbar_chiclet"
|
||||
},
|
||||
"trigger": {
|
||||
"type": "object",
|
||||
@ -123,5 +126,5 @@
|
||||
"required": ["id"]
|
||||
}
|
||||
},
|
||||
"required": ["id", "groups", "content", "targeting", "template", "trigger"]
|
||||
"required": ["id", "content", "targeting", "template", "trigger"]
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||
"$id": "file:///ExtensionDoorhanger.schema.json",
|
||||
"title": "ExtensionDoorhanger",
|
||||
"description": "A template with a heading, addon icon, title and description. No markup allowed.",
|
||||
"version": "1.0.0",
|
||||
@ -87,8 +89,22 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tooltiptext": {
|
||||
"type": "string",
|
||||
"description": "Text for button tooltip used to provide information about the doorhanger."
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Text for button tooltip used to provide information about the doorhanger."
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"string_id": {
|
||||
"type": "string",
|
||||
"description": "Id of localized string for the tooltip."
|
||||
}
|
||||
},
|
||||
"required": ["string_id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["tooltiptext"]
|
||||
@ -390,13 +406,24 @@
|
||||
"additionalProperties": true,
|
||||
"required": [
|
||||
"layout",
|
||||
"category",
|
||||
"bucket_id",
|
||||
"notification_text",
|
||||
"heading_text",
|
||||
"text",
|
||||
"buttons"
|
||||
]
|
||||
],
|
||||
"if": {
|
||||
"properties": {
|
||||
"skip_address_bar_notifier": {
|
||||
"anyOf": [
|
||||
{ "const": "false" },
|
||||
{ "const": null }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": ["category", "notification_text"]
|
||||
}
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
@ -406,7 +433,8 @@
|
||||
"description": "A JEXL expression representing targeting information"
|
||||
},
|
||||
"template": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"enum": ["cfr_doorhanger", "milestone_message"]
|
||||
},
|
||||
"trigger": {
|
||||
"type": "object",
|
||||
@ -428,5 +456,5 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"required": ["id", "groups", "content", "targeting", "template", "trigger"]
|
||||
"required": ["id", "content", "targeting", "template", "trigger"]
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||
"$id": "file:///InfoBar.schema.json",
|
||||
"title": "InfoBar",
|
||||
"description": "A template with an image, test and buttons.",
|
||||
"version": "1.0.0",
|
||||
@ -169,7 +171,8 @@
|
||||
"description": "A JEXL expression representing targeting information"
|
||||
},
|
||||
"template": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"const": "infobar"
|
||||
},
|
||||
"trigger": {
|
||||
"type": "object",
|
||||
@ -191,5 +194,5 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"required": ["id", "groups", "content", "targeting", "template", "trigger"]
|
||||
"required": ["id", "content", "targeting", "template", "trigger"]
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||
"$id": "file:///Spotlight.schema.json",
|
||||
"title": "Spotlight",
|
||||
"description": "A template with an image, title, content and two buttons.",
|
||||
"version": "1.1.0",
|
||||
@ -282,7 +284,8 @@
|
||||
"description": "A JEXL expression representing targeting information"
|
||||
},
|
||||
"template": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"const": "spotlight"
|
||||
},
|
||||
"trigger": {
|
||||
"type": "object",
|
||||
@ -304,5 +307,5 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"required": ["id", "groups", "content", "targeting", "template", "trigger"]
|
||||
"required": ["id", "content", "targeting", "template"]
|
||||
}
|
||||
|
@ -1,39 +1,63 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||
"$id": "file:///ToolbarBadgeMessage.schema.json",
|
||||
"title": "ToolbarBadgeMessage",
|
||||
"description": "A template that specifies to which element in the browser toolbar to add a notification.",
|
||||
"version": "1.1.0",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"content": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string"
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"description": "Optional action to take in addition to showing the notification"
|
||||
},
|
||||
"delay": {
|
||||
"type": "number",
|
||||
"description": "Optional delay in ms after which to show the notification"
|
||||
},
|
||||
"badgeDescription": {
|
||||
"type": "object",
|
||||
"description": "This is used in combination with the badged button to offer a text based alternative to the visual badging. Example 'New Feature: What's New'",
|
||||
"properties": {
|
||||
"string_id": {
|
||||
"type": "string",
|
||||
"description": "Fluent string id"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"string_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"required": ["target"]
|
||||
},
|
||||
"targeting": {
|
||||
"type": "string"
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["id"],
|
||||
"description": "Optional action to take in addition to showing the notification"
|
||||
},
|
||||
"delay": {
|
||||
"type": "number",
|
||||
"description": "Optional delay in ms after which to show the notification"
|
||||
},
|
||||
"badgeDescription": {
|
||||
"type": "object",
|
||||
"description": "This is used in combination with the badged button to offer a text based alternative to the visual badging. Example 'New Feature: What's New'",
|
||||
"properties": {
|
||||
"string_id": {
|
||||
"type": "string",
|
||||
"description": "Fluent string id"
|
||||
}
|
||||
},
|
||||
"required": ["string_id"]
|
||||
"template": {
|
||||
"type": "string",
|
||||
"const": "toolbar_badge"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["target"]
|
||||
"additionalProperties": true,
|
||||
"required": [
|
||||
"content",
|
||||
"targeting",
|
||||
"template"
|
||||
]
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||
"$id": "file:///UpdateAction.schema.json",
|
||||
"title": "UpdateActionMessage",
|
||||
"description": "A template for messages that execute predetermined actions.",
|
||||
"version": "1.0.0",
|
||||
@ -54,7 +55,8 @@
|
||||
"description": "A JEXL expression representing targeting information"
|
||||
},
|
||||
"template": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"const": "update_action"
|
||||
},
|
||||
"trigger": {
|
||||
"type": "object",
|
||||
@ -74,5 +76,6 @@
|
||||
},
|
||||
"required": ["id"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["id", "content", "targeting", "template"]
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||
"$id": "file:///WhatsNewMessage.schema.json",
|
||||
"title": "WhatsNewMessage",
|
||||
"description": "A template for the messages that appear in the What's New panel.",
|
||||
"version": "1.2.0",
|
||||
@ -17,81 +19,141 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["string_id"],
|
||||
"required": [
|
||||
"string_id"
|
||||
],
|
||||
"description": "Id of localized string to be rendered."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"layout": {
|
||||
"description": "Different message layouts",
|
||||
"enum": ["tracking-protections"]
|
||||
"content": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"layout": {
|
||||
"description": "Different message layouts",
|
||||
"enum": [
|
||||
"tracking-protections"
|
||||
]
|
||||
},
|
||||
"layout_title_content_variable": {
|
||||
"description": "Select what profile specific value to show for the current layout.",
|
||||
"type": "string"
|
||||
},
|
||||
"bucket_id": {
|
||||
"type": "string",
|
||||
"description": "A bucket identifier for the addon. This is used in order to anonymize telemetry for history-sensitive targeting."
|
||||
},
|
||||
"published_date": {
|
||||
"type": "integer",
|
||||
"description": "The date/time (number of milliseconds elapsed since January 1, 1970 00:00:00 UTC) the message was published."
|
||||
},
|
||||
"title": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/localizableText"
|
||||
},
|
||||
{
|
||||
"description": "Id of localized string or message override of What's New message title"
|
||||
}
|
||||
]
|
||||
},
|
||||
"subtitle": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/localizableText"
|
||||
},
|
||||
{
|
||||
"description": "Id of localized string or message override of What's New message subtitle"
|
||||
}
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/localizableText"
|
||||
},
|
||||
{
|
||||
"description": "Id of localized string or message override of What's New message body"
|
||||
}
|
||||
]
|
||||
},
|
||||
"link_text": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/localizableText"
|
||||
},
|
||||
{
|
||||
"description": "(optional) Id of localized string or message override of What's New message link text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cta_url": {
|
||||
"description": "Target URL for the What's New message.",
|
||||
"type": "string",
|
||||
"format": "moz-url-format"
|
||||
},
|
||||
"cta_type": {
|
||||
"description": "Type of url open action",
|
||||
"enum": [
|
||||
"OPEN_URL",
|
||||
"OPEN_ABOUT_PAGE",
|
||||
"OPEN_PROTECTION_REPORT"
|
||||
]
|
||||
},
|
||||
"cta_where": {
|
||||
"description": "How to open the cta: new window, tab, focused, unfocused.",
|
||||
"enum": [
|
||||
"current",
|
||||
"tabshifted",
|
||||
"tab",
|
||||
"save",
|
||||
"window"
|
||||
]
|
||||
},
|
||||
"icon_url": {
|
||||
"description": "(optional) URL for the What's New message icon.",
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"icon_alt": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/localizableText"
|
||||
},
|
||||
{
|
||||
"description": "Alt text for image."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"required": [
|
||||
"published_date",
|
||||
"title",
|
||||
"body",
|
||||
"cta_url",
|
||||
"bucket_id"
|
||||
],
|
||||
"dependencies": {
|
||||
"layout": [
|
||||
"layout_title_content_variable"
|
||||
]
|
||||
}
|
||||
},
|
||||
"layout_title_content_variable": {
|
||||
"description": "Select what profile specific value to show for the current layout.",
|
||||
"type": "string"
|
||||
"order": {
|
||||
"type": "integer"
|
||||
},
|
||||
"bucket_id": {
|
||||
"template": {
|
||||
"type": "string",
|
||||
"description": "A bucket identifier for the addon. This is used in order to anonymize telemetry for history-sensitive targeting."
|
||||
},
|
||||
"published_date": {
|
||||
"type": "integer",
|
||||
"description": "The date/time (number of milliseconds elapsed since January 1, 1970 00:00:00 UTC) the message was published."
|
||||
},
|
||||
"title": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/localizableText"},
|
||||
{"description": "Id of localized string or message override of What's New message title"}
|
||||
]
|
||||
},
|
||||
"subtitle": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/localizableText"},
|
||||
{"description": "Id of localized string or message override of What's New message subtitle"}
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/localizableText"},
|
||||
{"description": "Id of localized string or message override of What's New message body"}
|
||||
]
|
||||
},
|
||||
"link_text": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/localizableText"},
|
||||
{"description": "(optional) Id of localized string or message override of What's New message link text"}
|
||||
]
|
||||
},
|
||||
"cta_url": {
|
||||
"description": "Target URL for the What's New message.",
|
||||
"type": "string",
|
||||
"format": "moz-url-format"
|
||||
},
|
||||
"cta_type": {
|
||||
"description": "Type of url open action",
|
||||
"enum": ["OPEN_URL", "OPEN_ABOUT_PAGE", "OPEN_PROTECTION_REPORT"]
|
||||
},
|
||||
"cta_where": {
|
||||
"description": "How to open the cta: new window, tab, focused, unfocused.",
|
||||
"enum": ["current", "tabshifted", "tab", "save", "window"]
|
||||
},
|
||||
"icon_url": {
|
||||
"description": "(optional) URL for the What's New message icon.",
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"icon_alt": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/localizableText"},
|
||||
{"description": "Alt text for image."}
|
||||
]
|
||||
"const": "whatsnew_panel_message"
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"required": ["published_date", "title", "body", "cta_url", "bucket_id"],
|
||||
"dependencies": {
|
||||
"layout": ["layout_title_content_variable"]
|
||||
}
|
||||
"required": [
|
||||
"content",
|
||||
"order",
|
||||
"template"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
|
@ -1,88 +1,188 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||
"$id": "file:///NewtabPromoMessage.schema.json",
|
||||
"title": "PBNewtabPromoMessage",
|
||||
"description": "Message shown on the private browsing newtab page.",
|
||||
"version": "1.0.0",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"infoEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "Should we show the info section."
|
||||
},
|
||||
"infoIcon": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Icon shown in the left side of the info section. Default is the private browsing icon."
|
||||
"description": "Message identifier"
|
||||
},
|
||||
"infoTitle": {
|
||||
"groups": {
|
||||
"description": "Array of preferences used to control `enabled` status of the group. If any is `false` the group is disabled.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "Preference name"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hideDefault": {
|
||||
"type": "boolean",
|
||||
"description": "Should we hide the default promo after the experiment promo is dismissed."
|
||||
},
|
||||
"infoEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "Should we show the info section."
|
||||
},
|
||||
"infoIcon": {
|
||||
"type": "string",
|
||||
"description": "Icon shown in the left side of the info section. Default is the private browsing icon."
|
||||
},
|
||||
"infoTitle": {
|
||||
"type": "string",
|
||||
"description": "Is the title in the info section enabled."
|
||||
},
|
||||
"infoTitleEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "Is the title in the info section enabled."
|
||||
},
|
||||
"infoBody": {
|
||||
"type": "string",
|
||||
"description": "Text content in the info section."
|
||||
},
|
||||
"infoLinkText": {
|
||||
"type": "string",
|
||||
"description": "Text for the link in the info section."
|
||||
},
|
||||
"infoLinkUrl": {
|
||||
"type": "string",
|
||||
"description": "URL for the info section link.",
|
||||
"format": "moz-url-format"
|
||||
},
|
||||
"promoEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "Should we show the promo section."
|
||||
},
|
||||
"promoType": {
|
||||
"type": "string",
|
||||
"description": "Promo type used to determine if promo should show to a given user",
|
||||
"enum": [
|
||||
"FOCUS",
|
||||
"RALLY",
|
||||
"VPN"
|
||||
]
|
||||
},
|
||||
"promoSectionStyle": {
|
||||
"type": "string",
|
||||
"description": "Sets the position of the promo section. Possible values are: top, below-search, bottom. Default bottom.",
|
||||
"enum": [
|
||||
"top",
|
||||
"below-search",
|
||||
"bottom"
|
||||
]
|
||||
},
|
||||
"promoTitle": {
|
||||
"type": "string",
|
||||
"description": "The text content of the promo section."
|
||||
},
|
||||
"promoTitleEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "Should we show text content in the promo section."
|
||||
},
|
||||
"promoLinkText": {
|
||||
"type": "string",
|
||||
"description": "The text of the link in the promo box."
|
||||
},
|
||||
"promoHeader": {
|
||||
"type": "string",
|
||||
"description": "The title of the promo section."
|
||||
},
|
||||
"promoButton": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Action dispatched by the button."
|
||||
},
|
||||
"data": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": ["type"],
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": ["action"]
|
||||
},
|
||||
"promoLinkType": {
|
||||
"type": "string",
|
||||
"description": "Type of promo link type. Possible values: link, button. Default is link.",
|
||||
"enum": [
|
||||
"link",
|
||||
"button"
|
||||
]
|
||||
},
|
||||
"promoImageLarge": {
|
||||
"type": "string",
|
||||
"description": "URL for image used on the left side of the promo box, larger, showcases some feature. Default off.",
|
||||
"format": "uri"
|
||||
},
|
||||
"promoImageSmall": {
|
||||
"type": "string",
|
||||
"description": "URL for image used on the right side of the promo box, smaller, usually a logo. Default off.",
|
||||
"format": "uri"
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"promoEnabled": { "const": true }
|
||||
},
|
||||
"required": ["promoEnabled"]
|
||||
},
|
||||
"then": {
|
||||
"required": ["promoButton"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"infoEnabled": { "const": true }
|
||||
},
|
||||
"required": ["infoEnabled"]
|
||||
},
|
||||
"then": {
|
||||
"required": ["infoLinkText"],
|
||||
"if": {
|
||||
"properties": {
|
||||
"infoTitleEnabled": { "const": true }
|
||||
},
|
||||
"required": ["infoTitleEnabled"]
|
||||
},
|
||||
"then": {
|
||||
"required": ["infoTitle"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"targeting": {
|
||||
"type": "string",
|
||||
"description": "Is the title in the info section enabled."
|
||||
"description": "A JEXL expression represetning targeting information"
|
||||
},
|
||||
"infoTitleEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "Is the title in the info section enabled."
|
||||
},
|
||||
"infoBody": {
|
||||
"template": {
|
||||
"type": "string",
|
||||
"description": "Text content in the info section."
|
||||
},
|
||||
"infoLinkText": {
|
||||
"type": "string",
|
||||
"description": "Text for the link in the info section."
|
||||
},
|
||||
"infoLinkUrl": {
|
||||
"type": "string",
|
||||
"description": "URL for the info section link.",
|
||||
"format": "moz-url-format"
|
||||
},
|
||||
"promoEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "Should we show the promo section."
|
||||
},
|
||||
"promoType": {
|
||||
"type": "string",
|
||||
"description": "Promo type used to determine if promo should show to a given user",
|
||||
"enum": ["FOCUS", "RALLY", "VPN"]
|
||||
},
|
||||
"promoSectionStyle": {
|
||||
"type": "string",
|
||||
"description": "Sets the position of the promo section. Possible values are: top, below-search, bottom. Default bottom.",
|
||||
"enum": ["top", "below-search", "bottom"]
|
||||
},
|
||||
"promoTitle": {
|
||||
"type": "string",
|
||||
"description": "The text content of the promo section."
|
||||
},
|
||||
"promoTitleEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "Should we show text content in the promo section."
|
||||
},
|
||||
"promoLinkText": {
|
||||
"type": "string",
|
||||
"description": "The text of the link in the promo box."
|
||||
},
|
||||
"promoHeader": {
|
||||
"type": "string",
|
||||
"description": "The title of the promo section."
|
||||
},
|
||||
"promoLinkUrl": {
|
||||
"type": "string",
|
||||
"description": "URL for link in the promo box.",
|
||||
"format": "moz-url-format"
|
||||
},
|
||||
"promoLinkType": {
|
||||
"type": "string",
|
||||
"description": "Type of promo link type. Possible values: link, button. Default is link.",
|
||||
"enum": ["link", "button"]
|
||||
},
|
||||
"promoImageLarge": {
|
||||
"type": "string",
|
||||
"description": "URL for image used on the left side of the promo box, larger, showcases some feature. Default off.",
|
||||
"format": "uri"
|
||||
},
|
||||
"promoImageSmall": {
|
||||
"type": "string",
|
||||
"description": "URL for image used on the right side of the promo box, smaller, usually a logo. Default off.",
|
||||
"format": "uri"
|
||||
"const": "pb_newtab"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"content",
|
||||
"targeting",
|
||||
"template"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
|
@ -11,12 +11,7 @@ browser.jar:
|
||||
res/activity-stream/aboutwelcome/aboutwelcome.bundle.js (./aboutwelcome/content/aboutwelcome.bundle.js)
|
||||
res/activity-stream/aboutwelcome/aboutwelcome.html (./aboutwelcome/content/aboutwelcome.html)
|
||||
res/activity-stream/aboutwelcome/lib/ (./aboutwelcome/lib/*)
|
||||
res/activity-stream/schemas/CFR/ExtensionDoorhanger.schema.json (./content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json)
|
||||
res/activity-stream/schemas/CFR/InfoBar.schema.json (./content-src/asrouter/templates/CFR/templates/InfoBar.schema.json)
|
||||
res/activity-stream/schemas/OnboardingMessage/UpdateAction.schema.json (./content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json)
|
||||
res/activity-stream/schemas/OnboardingMessage/Spotlight.schema.json (./content-src/asrouter/templates/OnboardingMessage/Spotlight.schema.json)
|
||||
res/activity-stream/schemas/OnboardingMessage/WhatsNewMessage.schema.json (./content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json)
|
||||
res/activity-stream/schemas/PBNewtab/NewtabPromoMessage.schema.json (./content-src/asrouter/templates/PBNewtab/NewtabPromoMessage.schema.json)
|
||||
res/activity-stream/schemas/MessagingExperiment.schema.json (./content-src/asrouter/schemas/MessagingExperiment.schema.json)
|
||||
res/activity-stream/vendor/Redux.jsm (./vendor/Redux.jsm)
|
||||
res/activity-stream/vendor/react.js (./vendor/react.js)
|
||||
res/activity-stream/vendor/react-dom.js (./vendor/react-dom.js)
|
||||
|
@ -328,9 +328,17 @@ const MessageLoaderUtils = {
|
||||
},
|
||||
|
||||
async _experimentsAPILoader(provider) {
|
||||
// Allow tests to override the set of featureIds
|
||||
const featureIds = provider.featureIds ?? [
|
||||
"cfr",
|
||||
"infobar",
|
||||
"moments-page",
|
||||
"pbNewtab",
|
||||
"spotlight",
|
||||
];
|
||||
let experiments = [];
|
||||
for (const featureId of provider.messageGroups) {
|
||||
let FeatureAPI = lazy.NimbusFeatures[featureId];
|
||||
for (const featureId of featureIds) {
|
||||
let featureAPI = lazy.NimbusFeatures[featureId];
|
||||
let experimentData = lazy.ExperimentAPI.getExperimentMetaData({
|
||||
featureId,
|
||||
});
|
||||
@ -339,9 +347,13 @@ const MessageLoaderUtils = {
|
||||
continue;
|
||||
}
|
||||
|
||||
let message = FeatureAPI.getAllVariables();
|
||||
let message = featureAPI.getAllVariables();
|
||||
|
||||
if (message?.id) {
|
||||
// Cache the Nimbus feature ID on the message because there is not a 1-1
|
||||
// correspondance between templates and features. This is used when
|
||||
// recording expose events (see |sendTriggerMessage|).
|
||||
message._nimbusFeature = featureId;
|
||||
experiments.push(message);
|
||||
}
|
||||
|
||||
@ -1694,7 +1706,7 @@ class _ASRouter {
|
||||
TelemetryStopwatch.finish("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
|
||||
|
||||
// Format urls if any are defined
|
||||
["infoLinkUrl", "promoLinkUrl"].forEach(key => {
|
||||
["infoLinkUrl"].forEach(key => {
|
||||
if (message?.content?.[key]) {
|
||||
message.content[key] = Services.urlFormatter.formatURL(
|
||||
message.content[key]
|
||||
@ -1779,17 +1791,9 @@ class _ASRouter {
|
||||
}
|
||||
|
||||
if (nonReachMessages.length) {
|
||||
// Map from message template to Nimbus feature
|
||||
let featureMap = {
|
||||
cfr_doorhanger: "cfr",
|
||||
spotlight: "spotlight",
|
||||
infobar: "infobar",
|
||||
update_action: "moments-page",
|
||||
pb_newtab: "pbNewtab",
|
||||
};
|
||||
let feature = featureMap[nonReachMessages[0].template];
|
||||
if (feature) {
|
||||
lazy.NimbusFeatures[feature].recordExposureEvent({ once: true });
|
||||
let featureId = nonReachMessages[0]._nimbusFeature;
|
||||
if (featureId) {
|
||||
lazy.NimbusFeatures[featureId].recordExposureEvent({ once: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,6 +160,7 @@ const MESSAGES = () => [
|
||||
content: {
|
||||
layout: "icon_and_message",
|
||||
category: "cfrFeatures",
|
||||
bucket_id: "PERSONALIZED_CFR_MESSAGE",
|
||||
notification_text: "Personalized CFR Recommendation",
|
||||
heading_text: { string_id: "cfr-doorhanger-bookmark-fxa-header" },
|
||||
info_icon: {
|
||||
@ -635,8 +636,6 @@ const MESSAGES = () => [
|
||||
promoEnabled: true,
|
||||
promoType: "VPN",
|
||||
infoEnabled: true,
|
||||
infoIcon: "",
|
||||
infoTitle: "",
|
||||
infoBody: "fluent:about-private-browsing-info-description-private-window",
|
||||
infoLinkText: "fluent:about-private-browsing-learn-more-link",
|
||||
infoTitleEnabled: false,
|
||||
@ -647,7 +646,16 @@ const MESSAGES = () => [
|
||||
promoTitle: "fluent:about-private-browsing-hide-activity-1",
|
||||
promoTitleEnabled: true,
|
||||
promoImageLarge: "chrome://browser/content/assets/moz-vpn.svg",
|
||||
promoButton: {
|
||||
action: {
|
||||
type: "OPEN_URL",
|
||||
data: {
|
||||
args: "https://vpn.mozilla.org/",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
groups: ["panel-test-provider"],
|
||||
targeting: "region != 'CN' && !hasActiveEnterprisePolicies",
|
||||
frequency: { lifetime: 3 },
|
||||
},
|
||||
|
@ -13,6 +13,12 @@ BROWSER_CHROME_MANIFESTS += [
|
||||
]
|
||||
|
||||
TESTING_JS_MODULES += [
|
||||
"content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json",
|
||||
"content-src/asrouter/templates/OnboardingMessage/Spotlight.schema.json",
|
||||
"content-src/asrouter/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json",
|
||||
"content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json",
|
||||
"content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json",
|
||||
"content-src/asrouter/templates/PBNewtab/NewtabPromoMessage.schema.json",
|
||||
"test/RemoteImagesTestUtils.jsm",
|
||||
]
|
||||
|
||||
|
@ -180,7 +180,7 @@ async function setup(experiment) {
|
||||
["datareporting.healthreport.uploadEnabled", true],
|
||||
[
|
||||
"browser.newtabpage.activity-stream.asrouter.providers.messaging-experiments",
|
||||
`{"id":"messaging-experiments","enabled":true,"type":"remote-experiments","messageGroups":["cfr","spotlight","infobar","aboutwelcome"],"updateCycleInMs":0}`,
|
||||
`{"id":"messaging-experiments","enabled":true,"type":"remote-experiments","updateCycleInMs":0}`,
|
||||
],
|
||||
],
|
||||
});
|
||||
|
@ -223,9 +223,10 @@ describe("ASRouter", () => {
|
||||
};
|
||||
let fakeNimbusFeatures = [
|
||||
"cfr",
|
||||
"moments-page",
|
||||
"infobar",
|
||||
"spotlight",
|
||||
"moments-page",
|
||||
"pbNewtab",
|
||||
].reduce((features, featureId) => {
|
||||
features[featureId] = {
|
||||
getAllVariables: sandbox.stub().returns(null),
|
||||
@ -1736,11 +1737,11 @@ describe("ASRouter", () => {
|
||||
|
||||
describe("#reachEvent", () => {
|
||||
let experimentAPIStub;
|
||||
let messageGroups = ["cfr", "moments-page", "infobar", "spotlight"];
|
||||
let featureIds = ["cfr", "moments-page", "infobar", "spotlight"];
|
||||
beforeEach(() => {
|
||||
let getExperimentMetaDataStub = sandbox.stub();
|
||||
let getAllBranchesStub = sandbox.stub();
|
||||
messageGroups.forEach(feature => {
|
||||
featureIds.forEach(feature => {
|
||||
global.NimbusFeatures[feature].getAllVariables.returns({
|
||||
id: `message-${feature}`,
|
||||
});
|
||||
@ -1770,15 +1771,15 @@ describe("ASRouter", () => {
|
||||
// This should match the `providers.messaging-experiments`
|
||||
let response = await MessageLoaderUtils.loadMessagesForProvider({
|
||||
type: "remote-experiments",
|
||||
messageGroups,
|
||||
featureIds,
|
||||
});
|
||||
|
||||
// 1 message for reach 1 for expose
|
||||
assert.property(response, "messages");
|
||||
assert.lengthOf(response.messages, messageGroups.length * 2);
|
||||
assert.lengthOf(response.messages, featureIds.length * 2);
|
||||
assert.lengthOf(
|
||||
response.messages.filter(m => m.forReachEvent),
|
||||
messageGroups.length
|
||||
featureIds.length
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -2621,7 +2622,7 @@ describe("ASRouter", () => {
|
||||
it("should fetch messages from the ExperimentAPI", async () => {
|
||||
const args = {
|
||||
type: "remote-experiments",
|
||||
messageGroups: ["spotlight"],
|
||||
featureIds: ["spotlight"],
|
||||
};
|
||||
|
||||
await MessageLoaderUtils.loadMessagesForProvider(args);
|
||||
@ -2635,7 +2636,7 @@ describe("ASRouter", () => {
|
||||
it("should handle the case of no experiments in the ExperimentAPI", async () => {
|
||||
const args = {
|
||||
type: "remote-experiments",
|
||||
messageGroups: ["infobar"],
|
||||
featureIds: ["infobar"],
|
||||
};
|
||||
|
||||
global.ExperimentAPI.getExperiment.returns(null);
|
||||
@ -2647,7 +2648,7 @@ describe("ASRouter", () => {
|
||||
it("should normally load ExperimentAPI messages", async () => {
|
||||
const args = {
|
||||
type: "remote-experiments",
|
||||
messageGroups: ["infobar"],
|
||||
featureIds: ["infobar"],
|
||||
};
|
||||
const enrollment = {
|
||||
branch: {
|
||||
@ -2683,7 +2684,7 @@ describe("ASRouter", () => {
|
||||
it("should skip disabled features and not load the messages", async () => {
|
||||
const args = {
|
||||
type: "remote-experiments",
|
||||
messageGroups: ["cfr"],
|
||||
featureIds: ["cfr"],
|
||||
};
|
||||
|
||||
global.NimbusFeatures.cfr.getAllVariables.returns(null);
|
||||
@ -2695,7 +2696,7 @@ describe("ASRouter", () => {
|
||||
it("should fetch branches with trigger", async () => {
|
||||
const args = {
|
||||
type: "remote-experiments",
|
||||
messageGroups: ["cfr"],
|
||||
featureIds: ["cfr"],
|
||||
};
|
||||
const enrollment = {
|
||||
slug: "exp01",
|
||||
@ -2750,7 +2751,7 @@ describe("ASRouter", () => {
|
||||
it("should fetch branches with trigger even if enrolled branch is disabled", async () => {
|
||||
const args = {
|
||||
type: "remote-experiments",
|
||||
messageGroups: ["cfr"],
|
||||
featureIds: ["cfr"],
|
||||
};
|
||||
const enrollment = {
|
||||
slug: "exp01",
|
||||
@ -2763,7 +2764,7 @@ describe("ASRouter", () => {
|
||||
},
|
||||
};
|
||||
|
||||
// Nedds to match the `messageGroups` value to return an enrollment
|
||||
// Nedds to match the `featureIds` value to return an enrollment
|
||||
// for that feature
|
||||
global.NimbusFeatures.cfr.getAllVariables.returns(
|
||||
enrollment.branch.cfr.value
|
||||
|
@ -1,11 +1,13 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
OnboardingMessageProvider:
|
||||
"resource://activity-stream/lib/OnboardingMessageProvider.jsm",
|
||||
sinon: "resource://testing-common/Sinon.jsm",
|
||||
});
|
||||
const { JsonSchema } = ChromeUtils.import(
|
||||
"resource://gre/modules/JsonSchema.jsm"
|
||||
);
|
||||
const { OnboardingMessageProvider } = ChromeUtils.import(
|
||||
"resource://activity-stream/lib/OnboardingMessageProvider.jsm"
|
||||
);
|
||||
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
|
||||
|
||||
add_task(
|
||||
async function test_OnboardingMessageProvider_getUpgradeMessage_no_pin() {
|
||||
@ -71,3 +73,64 @@ add_task(
|
||||
sandbox.restore();
|
||||
}
|
||||
);
|
||||
|
||||
add_task(async function test_schemaValidation() {
|
||||
function schemaValidator(uri) {
|
||||
return fetch(uri, { credentials: "omit" })
|
||||
.then(rsp => rsp.json())
|
||||
.then(schema => new JsonSchema.Validator(schema));
|
||||
}
|
||||
|
||||
function assertValid(validator, obj, msg) {
|
||||
const result = validator.validate(obj);
|
||||
Assert.deepEqual(
|
||||
result,
|
||||
{ valid: true, errors: [] },
|
||||
`${msg} - errors = ${JSON.stringify(result.errors, undefined, 2)}`
|
||||
);
|
||||
}
|
||||
|
||||
const experimentValidator = await schemaValidator(
|
||||
"resource://activity-stream/schemas/MessagingExperiment.schema.json"
|
||||
);
|
||||
const schemas = {
|
||||
toolbar_badge: await schemaValidator(
|
||||
"resource://testing-common/ToolbarBadgeMessage.schema.json"
|
||||
),
|
||||
cfr_doorhanger: await schemaValidator(
|
||||
"resource://testing-common/ExtensionDoorhanger.schema.json"
|
||||
),
|
||||
spotlight: await schemaValidator(
|
||||
"resource://testing-common/Spotlight.schema.json"
|
||||
),
|
||||
pb_newtab: await schemaValidator(
|
||||
"resource://testing-common/NewtabPromoMessage.schema.json"
|
||||
),
|
||||
protections_panel: null, // TODO: There is no schema for protections_panel.
|
||||
};
|
||||
|
||||
const messages = await OnboardingMessageProvider.getMessages();
|
||||
for (const message of messages) {
|
||||
const validator = schemas[message.template];
|
||||
|
||||
if (validator === null) {
|
||||
continue;
|
||||
} else if (typeof validator === "undefined") {
|
||||
Assert.ok(
|
||||
false,
|
||||
`No schema validator found for message template ${message.template}. Please update this test to add one.`
|
||||
);
|
||||
} else {
|
||||
assertValid(
|
||||
validator,
|
||||
message,
|
||||
`Message ${message.id} validates as template ${message.template}`
|
||||
);
|
||||
assertValid(
|
||||
experimentValidator,
|
||||
message,
|
||||
`Message ${message.id} validates as MessagingExperiment`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ const { JsonSchema } = ChromeUtils.import(
|
||||
|
||||
Cu.importGlobalProperties(["fetch"]);
|
||||
|
||||
let MESSAGING_EXPERIMENT_SCHEMA;
|
||||
let CFR_SCHEMA;
|
||||
let UPDATE_ACTION_SCHEMA;
|
||||
let WHATS_NEW_SCHEMA;
|
||||
@ -21,20 +22,23 @@ add_setup(async function setup() {
|
||||
return fetch(uri, { credentials: "omit" }).then(rsp => rsp.json());
|
||||
}
|
||||
|
||||
MESSAGING_EXPERIMENT_SCHEMA = await fetchSchema(
|
||||
"resource://activity-stream/schemas/MessagingExperiment.schema.json"
|
||||
);
|
||||
CFR_SCHEMA = await fetchSchema(
|
||||
"resource://activity-stream/schemas/CFR/ExtensionDoorhanger.schema.json"
|
||||
"resource://testing-common/ExtensionDoorhanger.schema.json"
|
||||
);
|
||||
UPDATE_ACTION_SCHEMA = await fetchSchema(
|
||||
"resource://activity-stream/schemas/OnboardingMessage/UpdateAction.schema.json"
|
||||
"resource://testing-common/UpdateAction.schema.json"
|
||||
);
|
||||
WHATS_NEW_SCHEMA = await fetchSchema(
|
||||
"resource://activity-stream/schemas/OnboardingMessage/WhatsNewMessage.schema.json"
|
||||
"resource://testing-common/WhatsNewMessage.schema.json"
|
||||
);
|
||||
SPOTLIGHT_SCHEMA = await fetchSchema(
|
||||
"resource://activity-stream/schemas/OnboardingMessage/Spotlight.schema.json"
|
||||
"resource://testing-common/Spotlight.schema.json"
|
||||
);
|
||||
PB_NEWTAB_SCHEMA = await fetchSchema(
|
||||
"resource://activity-stream/schemas/PBNewtab/NewtabPromoMessage.schema.json"
|
||||
"resource://testing-common/NewtabPromoMessage.schema.json"
|
||||
);
|
||||
});
|
||||
|
||||
@ -42,7 +46,13 @@ function assertSchema(obj, schema, log) {
|
||||
Assert.deepEqual(
|
||||
JsonSchema.validate(obj, schema),
|
||||
{ valid: true, errors: [] },
|
||||
log
|
||||
`${log} (${schema.title} schema)`
|
||||
);
|
||||
|
||||
Assert.deepEqual(
|
||||
JsonSchema.validate(obj, MESSAGING_EXPERIMENT_SCHEMA),
|
||||
{ valid: true, errors: [] },
|
||||
`${log} (MessagingExperiment schema)`
|
||||
);
|
||||
}
|
||||
|
||||
@ -57,6 +67,12 @@ add_task(async function test_PanelTestProvider() {
|
||||
"PanelTestProvider should have the correct number of messages"
|
||||
);
|
||||
|
||||
for (const [i, msg] of messages
|
||||
.filter(m => ["cfr_doorhanger", "milestone_message"].includes(m.template))
|
||||
.entries()) {
|
||||
assertSchema(msg, CFR_SCHEMA, `cfr message ${msg.id ?? i} is valid`);
|
||||
}
|
||||
|
||||
for (const [i, msg] of messages
|
||||
.filter(m => m.template === "update_action")
|
||||
.entries()) {
|
||||
@ -71,14 +87,10 @@ add_task(async function test_PanelTestProvider() {
|
||||
.filter(m => m.template === "whatsnew_panel_message")
|
||||
.entries()) {
|
||||
assertSchema(
|
||||
msg.content,
|
||||
msg,
|
||||
WHATS_NEW_SCHEMA,
|
||||
`whatsnew_panel_message message ${msg.id ?? i} is valid`
|
||||
);
|
||||
Assert.ok(
|
||||
Object.keys(msg).includes("order"),
|
||||
`whatsnew_panel_message message ${msg.id ?? i} has "order" property`
|
||||
);
|
||||
}
|
||||
|
||||
for (const [i, msg] of messages
|
||||
@ -107,35 +119,3 @@ add_task(async function test_PanelTestProvider() {
|
||||
"There is one pb_newtab message"
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_SpotlightAsCFR() {
|
||||
let message = await PanelTestProvider.getMessages().then(msgs =>
|
||||
msgs.find(msg => msg.id === "SPOTLIGHT_MESSAGE_93")
|
||||
);
|
||||
|
||||
message = {
|
||||
...message,
|
||||
content: {
|
||||
...message.content,
|
||||
category: "",
|
||||
layout: "icon_and_message",
|
||||
bucket_id: "",
|
||||
notification_text: "",
|
||||
heading_text: "",
|
||||
text: "",
|
||||
buttons: {},
|
||||
},
|
||||
};
|
||||
|
||||
assertSchema(
|
||||
message,
|
||||
CFR_SCHEMA,
|
||||
"Munged spotlight message validates with CFR ExtensionDoorhanger schema"
|
||||
);
|
||||
|
||||
assertSchema(
|
||||
message,
|
||||
SPOTLIGHT_SCHEMA,
|
||||
"Munged Spotlight message validates with Spotlight schema"
|
||||
);
|
||||
});
|
||||
|
@ -40,7 +40,7 @@ async function renderInfo({
|
||||
infoIcon,
|
||||
} = {}) {
|
||||
const container = document.querySelector(".info");
|
||||
if (infoEnabled === false) {
|
||||
if (!infoEnabled) {
|
||||
container.remove();
|
||||
return;
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ async function setupMSExperimentWithMessage(message) {
|
||||
set: [
|
||||
[
|
||||
"browser.newtabpage.activity-stream.asrouter.providers.messaging-experiments",
|
||||
'{"id":"messaging-experiments","enabled":true,"type":"remote-experiments","messageGroups":["pbNewtab"],"updateCycleInMs":0}',
|
||||
'{"id":"messaging-experiments","enabled":true,"type":"remote-experiments","updateCycleInMs":0}',
|
||||
],
|
||||
],
|
||||
});
|
||||
|
@ -422,6 +422,21 @@ mozperftest:
|
||||
- 'testing/performance/**'
|
||||
- 'python/mozperftest/**'
|
||||
|
||||
fxms-schemas:
|
||||
description: Ensure messaging-system schemas are up-to-date
|
||||
treeherder:
|
||||
symbol: py3(fxms)
|
||||
run:
|
||||
using: run-task
|
||||
cwd: '{checkout}'
|
||||
command: >
|
||||
cd browser/components/newtab/content-src/asrouter/schemas &&
|
||||
python3 make-schemas.py --check
|
||||
when:
|
||||
files-changed:
|
||||
- 'browser/components/newtab/content-src/asrouter/schemas/make-schemas.py'
|
||||
- 'browser/components/newtab/content-src/**/*.schema.json'
|
||||
|
||||
condprof:
|
||||
description: testing/condprofile unit tests
|
||||
platform:
|
||||
|
@ -8,6 +8,41 @@ More information about `Messaging System`__.
|
||||
|
||||
.. __: /browser/components/newtab/content-src/asrouter/docs
|
||||
|
||||
Messages
|
||||
--------
|
||||
|
||||
There are JSON schemas for each type of message that the Firefox Messaging
|
||||
System handles:
|
||||
|
||||
* `CFR URLBar Chiclet <cfr_urlbar_chiclet_schema_>`_
|
||||
* `Extension Doorhanger <extension_doorhanger_schema_>`_
|
||||
* `Infobar <infobar_schema_>`_
|
||||
* `Spotlight <spotlight_schema_>`_
|
||||
* `Toolbar Badge <toolbar_badge_schema_>`_
|
||||
* `Update Action <update_action_schema_>`_
|
||||
* `Whats New <whats_new_schema_>`_
|
||||
* `Private Browsing Newtab Promo Message <pbnewtab_promo_schema_>`_
|
||||
|
||||
Together, they are combined into the `Messaging Experiments
|
||||
<messaging_experiments_schema_>`_ via a `script <make_schemas_script_>`_. This
|
||||
is the schema used for Nimbus experiments that target messaging features. All
|
||||
incoming messaging experiments will be validated against these schemas.
|
||||
|
||||
To add a new message type to the Messaging Experiments schema:
|
||||
|
||||
1. Ensure the schema has an ``$id`` member. This allows for references (e.g.,
|
||||
``{ "$ref": "#!/$defs/Foo" }``) to work in the bundled schema. See docs on
|
||||
`bundling JSON schemas <jsonschema_bundling_>`_ for more information.
|
||||
2. Add the new schema to the list in `make-schemas.py <make_schemas_script_>`_.
|
||||
3. Build the new schema by running:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
cd browser/components/newtab/schemas
|
||||
../../../../../../mach python make-schemas.py
|
||||
|
||||
4. Commit the results.
|
||||
|
||||
Triggers and actions
|
||||
---------------------
|
||||
|
||||
@ -16,3 +51,15 @@ Triggers and actions
|
||||
|
||||
SpecialMessageActionSchemas/index
|
||||
TriggerActionSchemas/index
|
||||
|
||||
.. _cfr_urlbar_chiclet_schema: https://searchfox.org/mozilla-central/source/browser/components/newtab/content-src/asrouter/templates/CFR/templates/CFRUrlbarChiclet.schema.json
|
||||
.. _extension_doorhanger_schema: https://searchfox.org/mozilla-central/source/browser/components/newtab/content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json
|
||||
.. _infobar_schema: https://searchfox.org/mozilla-central/source/browser/components/newtab/content-src/asrouter/templates/CFR/templates/InfoBar.schema.json
|
||||
.. _spotlight_schema: https://searchfox.org/mozilla-central/source/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/Spotlight.schema.json
|
||||
.. _toolbar_badge_schema: https://searchfox.org/mozilla-central/source/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json
|
||||
.. _update_action_schema: https://searchfox.org/mozilla-central/source/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json
|
||||
.. _whats_new_schema: https://searchfox.org/mozilla-central/source/browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json
|
||||
.. _pbnewtab_promo_schema: https://searchfox.org/mozilla-central/source/browser/components/newtab/content-src/asrouter/templates/PBNewtab/NewtabPromoMessage.schema.json
|
||||
.. _messaging_experiments_schema: https://searchfox.org/mozilla-central/source/browser/components/newtab/schemas/MessagingExperiment.schema.json
|
||||
.. _make_schemas_script: https://searchfox.org/mozilla-central/source/browser/components/newtab/content-src/asrouter/schemas/make-schemas.py
|
||||
.. _jsonschema_bundling: https://json-schema.org/understanding-json-schema/structuring.html#bundling
|
||||
|
@ -456,53 +456,53 @@ tcpByDefault:
|
||||
type: boolean
|
||||
description: "Enables TCP by default"
|
||||
cfr:
|
||||
description: "Doorhanger message template for Messaging System"
|
||||
description: "A Firefox Messaging System message for the cfr message channel"
|
||||
hasExposure: true
|
||||
exposureDescription: "Exposure is sent if the message is about to be shown after trigger and targeting conditions on the message matched."
|
||||
isEarlyStartup: false
|
||||
schema:
|
||||
uri: "resource://activity-stream/schemas/CFR/ExtensionDoorhanger.schema.json"
|
||||
path: "browser/components/newtab/content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json"
|
||||
uri: "resource://activity-stream/schemas/MessagingExperiment.schema.json"
|
||||
path: "browser/components/newtab/schemas/MessagingExperiment.schema.json"
|
||||
variables: {}
|
||||
"moments-page":
|
||||
description: "Message with URL data for Messaging System"
|
||||
description: "A Firefox Messaging System message for the moments-page message channel"
|
||||
hasExposure: true
|
||||
exposureDescription: >-
|
||||
"Exposure is sent if the message is about to be shown after trigger and targeting conditions on the message matched."
|
||||
isEarlyStartup: false
|
||||
schema:
|
||||
uri: "resource://activity-stream/schemas/OnboardingMessage/UpdateAction.schema.json"
|
||||
path: "browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json"
|
||||
uri: "resource://activity-stream/schemas/MessagingExperiment.schema.json"
|
||||
path: "browser/components/newtab/schemas/MessagingExperiment.schema.json"
|
||||
variables: {}
|
||||
infobar:
|
||||
description: "Message template for Messaging System"
|
||||
description: "A Firefox Messaging system message for the infobar message channel"
|
||||
hasExposure: true
|
||||
exposureDescription: >-
|
||||
"Exposure is sent if the message is about to be shown after trigger and targeting conditions on the message matched."
|
||||
isEarlyStartup: false
|
||||
schema:
|
||||
uri: "resource://activity-stream/schemas/CFR/InfoBar.schema.json"
|
||||
path: "browser/components/newtab/content-src/asrouter/templates/CFR/templates/InfoBar.schema.json"
|
||||
uri: "resource://activity-stream/schemas/MessagingExperiment.schema.json"
|
||||
path: "browser/components/newtab/schemas/MessagingExperiment.schema.json"
|
||||
variables: {}
|
||||
spotlight:
|
||||
description: "Modal message template for Messaging System"
|
||||
description: "A Firefox Messaging System message for the spotlight message channel"
|
||||
hasExposure: true
|
||||
exposureDescription: >-
|
||||
"Exposure is sent if the message is about to be shown after trigger and targeting conditions on the message matched."
|
||||
isEarlyStartup: false
|
||||
schema:
|
||||
uri: "resource://activity-stream/schemas/OnboardingMessage/Spotlight.schema.json"
|
||||
path: "browser/components/newtab/content-src/asrouter/templates/OnboardingMessage/Spotlight.schema.json"
|
||||
uri: "resource://activity-stream/schemas/MessagingExperiment.schema.json"
|
||||
path: "browser/components/newtab/schemas/MessagingExperiment.schema.json"
|
||||
variables: {}
|
||||
pbNewtab:
|
||||
description: Message shown on the PB newtab for Messaging System
|
||||
description: "A Firefox Messaging System message for the pbNewtab message channel"
|
||||
hasExposure: true
|
||||
exposureDescription: >-
|
||||
Exposure is sent if the message is about to be shown after trigger and targeting conditions on the message matched.
|
||||
isEarlyStartup: false
|
||||
schema:
|
||||
uri: "resource://activity-stream/schemas/PBNewtab/NewtabPromoMessage.schema.json"
|
||||
path: "browser/components/newtab/content-src/asrouter/templates/PBNewtab/NewtabPromoMessage.schema.json"
|
||||
uri: "resource://activity-stream/schemas/MessagingExperiment.schema.json"
|
||||
path: "browser/components/newtab/schemas/MessagingExperiment.schema.json"
|
||||
variables: {}
|
||||
syncAfterTabChange:
|
||||
description: "Schedule a sync after any tab change"
|
||||
|
Loading…
Reference in New Issue
Block a user