Bug 1500540 - Add end-of-year snippet, contextual-feature-recommender preference and bug fixes to Activity Stream r=k88hudson

Differential Revision: https://phabricator.services.mozilla.com/D9297

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Ed Lee 2018-10-19 22:59:47 +00:00
Родитель 9f4a9ec916
Коммит 310cf60a08
62 изменённых файлов: 2731 добавлений и 1403 удалений

Просмотреть файл

@ -6,17 +6,7 @@ import {LocalizationProvider} from "fluent-react";
import {OnboardingMessage} from "./templates/OnboardingMessage/OnboardingMessage";
import React from "react";
import ReactDOM from "react-dom";
import {SendToDeviceSnippet} from "./templates/SendToDeviceSnippet/SendToDeviceSnippet";
import {SimpleSnippet} from "./templates/SimpleSnippet/SimpleSnippet";
import {SubmitFormSnippet} from "./templates/SubmitFormSnippet/SubmitFormSnippet";
// Key names matching schema name of templates
const SnippetComponents = {
simple_snippet: SimpleSnippet,
newsletter_snippet: props => <SubmitFormSnippet {...props} form_method="POST" />,
fxa_signup_snippet: props => <SubmitFormSnippet {...props} form_method="GET" />,
send_to_device_snippet: SendToDeviceSnippet,
};
import {SnippetsTemplates} from "./templates/template-manifest";
const INCOMING_MESSAGE_NAME = "ASRouter:parent-to-child";
const OUTGOING_MESSAGE_NAME = "ASRouter:child-to-parent";
@ -196,7 +186,7 @@ export class ASRouterUISurface extends React.PureComponent {
}
renderSnippets() {
const SnippetComponent = SnippetComponents[this.state.message.template];
const SnippetComponent = SnippetsTemplates[this.state.message.template];
const {content} = this.state.message;
return (

Просмотреть файл

@ -17,7 +17,7 @@ const ALLOWED_TAGS = {
* Transform an object (tag name: {url}) into (tag name: anchor) where the url
* is used as href, in order to render links inside a Fluent.Localized component.
*/
export function convertLinks(links, sendClick, autoBlock) {
export function convertLinks(links, sendClick, doNotAutoBlock) {
if (links) {
return Object.keys(links).reduce((acc, linkTag) => {
const {action} = links[linkTag];
@ -25,11 +25,11 @@ export function convertLinks(links, sendClick, autoBlock) {
const url = action ? false : safeURI(links[linkTag].url);
acc[linkTag] = (<a href={url}
target={autoBlock === false ? "_blank" : ""}
target={doNotAutoBlock ? "_blank" : ""}
data-metric={links[linkTag].metric}
data-action={action}
data-args={links[linkTag].args}
data-do_not_autoblock={autoBlock === false}
data-do_not_autoblock={doNotAutoBlock}
onClick={sendClick} />);
return acc;
}, {});
@ -46,7 +46,7 @@ export function RichText(props) {
throw new Error(`ASRouter: ${props.localization_id} is not a valid rich text property. If you want it to be processed, you need to add it to asrouter/rich-text-strings.js`);
}
return (
<Localized id={props.localization_id} {...ALLOWED_TAGS} {...convertLinks(props.links, props.sendClick, props.autoBlock)}>
<Localized id={props.localization_id} {...ALLOWED_TAGS} {...props.customElements} {...convertLinks(props.links, props.sendClick, props.doNotAutoBlock)}>
<span>{props.text}</span>
</Localized>
);

Просмотреть файл

@ -30,7 +30,7 @@ export class SnippetBase extends React.PureComponent {
}
return (
<button className="blockButton" title={this.props.content.block_button_text} onClick={this.onBlockClicked} />
<button className="blockButton" title={this.props.content.block_button_text || "Remove this"} onClick={this.onBlockClicked} />
);
}
@ -39,7 +39,7 @@ export class SnippetBase extends React.PureComponent {
const containerClassName = `SnippetBaseContainer${props.className ? ` ${props.className}` : ""}`;
return (<div className={containerClassName}>
return (<div className={containerClassName} style={this.props.textStyle}>
<div className="innerWrapper">
{props.children}
</div>

Просмотреть файл

@ -15,6 +15,7 @@
a {
cursor: pointer;
color: var(--newtab-link-primary-color);
text-decoration: underline;
[lwt-newtab-brighttext] & {
font-weight: bold;

Просмотреть файл

@ -16,7 +16,8 @@ Please note that some targeting attributes require stricter controls on the tele
* [firefoxVersion](#firefoxversion)
* [locale](#locale)
* [localeLanguageCode](#localelanguagecode)
* [usesFirefoxSync](#usesfirefoxsync)
* [needsUpdate](#needsupdate)
* [pinnedSites](#pinnedsites)
* [previousSessionEnd](#previoussessionend)
* [profileAgeCreated](#profileagecreated)
* [profileAgeReset](#profileagereset)
@ -26,8 +27,8 @@ Please note that some targeting attributes require stricter controls on the tele
* [sync](#sync)
* [topFrecentSites](#topfrecentsites)
* [totalBookmarksCount](#totalbookmarkscount)
* [usesFirefoxSync](#usesfirefoxsync)
* [xpinstallEnabled](#xpinstallEnabled)
* [needsUpdate](#needsupdate)
## Detailed usage
@ -210,14 +211,39 @@ localeLanguageCode == "en"
declare const localeLanguageCode: string;
```
### `usesFirefoxSync`
### `needsUpdate`
Does the user use Firefox sync?
#### Definition
Does the client have the latest available version installed
```ts
declare const usesFirefoxSync: boolean;
declare const needsUpdate: boolean;
```
### `pinnedSites`
The sites (including search shortcuts) that are pinned on a user's new tab page.
#### Examples
* Has the user pinned any site on `foo.com`?
```java
"foo.com" in pinnedSites|mapToProperty("host")
```
* Does the user have a pinned `duckduckgo.com` search shortcut?
```java
"duckduckgo.com" in pinnedSites[.searchTopSite == true]|mapToProperty("host")
```
#### Definition
```ts
interface PinnedSite {
// e.g. https://foo.mozilla.com/foo/bar
url: string;
// e.g. foo.mozilla.com
host: string;
// is the pin a search shortcut?
searchTopSite: boolean;
}
declare const pinnedSites: Array<PinnedSite>
```
### `previousSessionEnd`
@ -376,6 +402,16 @@ Total number of bookmarks.
declare const totalBookmarksCount: number;
```
### `usesFirefoxSync`
Does the user use Firefox sync?
#### Definition
```ts
declare const usesFirefoxSync: boolean;
```
### `xpinstallEnabled`
Pref used by system administrators to disallow add-ons from installed altogether.
@ -385,11 +421,3 @@ Pref used by system administrators to disallow add-ons from installed altogether
```ts
declare const xpinstallEnabled: boolean;
```
### `needsUpdate`
Does the client have the latest available version installed
```ts
declare const needsUpdate: boolean;
```

Просмотреть файл

@ -0,0 +1,97 @@
import React from "react";
import {SimpleSnippet} from "../SimpleSnippet/SimpleSnippet";
class EOYSnippetBase extends React.PureComponent {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
/**
* setFrequencyValue - `frequency` form parameter value should be `monthly`
* if `monthly-checkbox` is selected or `single` otherwise
*/
setFrequencyValue() {
const frequencyCheckbox = this.refs.form.querySelector("#monthly-checkbox");
if (frequencyCheckbox.checked) {
this.refs.form.querySelector("[name='frequency']").value = "monthly";
}
}
handleSubmit(event) {
event.preventDefault();
this.setFrequencyValue();
this.refs.form.submit();
if (!this.props.content.do_not_autoblock) {
this.props.onBlock();
}
}
renderDonations() {
const fieldNames = ["first", "second", "third", "fourth"];
const numberFormat = new Intl.NumberFormat(this.props.content.locale || navigator.language, {
style: "currency",
currency: this.props.content.currency_code,
minimumFractionDigits: 0,
});
// Default to `second` button
const {selected_button} = this.props.content;
const btnStyle = {
color: this.props.content.button_color,
backgroundColor: this.props.content.button_background_color,
};
return (<form className="EOYSnippetForm" action={this.props.content.donation_form_url} method={this.props.form_method} onSubmit={this.handleSubmit} ref="form">
{fieldNames.map((field, idx) => {
const button_name = `donation_amount_${field}`;
const amount = this.props.content[button_name];
return (<React.Fragment key={idx}>
<input type="radio" name="amount" value={amount} id={field} defaultChecked={button_name === selected_button} />
<label htmlFor={field} className="donation-amount">
{numberFormat.format(amount)}
</label>
</React.Fragment>);
})}
<div className="monthly-checkbox-container">
<input id="monthly-checkbox" type="checkbox" />
<label htmlFor="monthly-checkbox">
{this.props.content.monthly_checkbox_label_text}
</label>
</div>
<input type="hidden" name="frequency" value="single" />
<input type="hidden" name="currency" value={this.props.content.currency_code} />
<input type="hidden" name="presets" value={fieldNames.map(field => this.props.content[`donation_amount_${field}`])} />
<button style={btnStyle} type="submit" className="ASRouterButton donation-form-url">{this.props.content.button_label}</button>
</form>);
}
render() {
const textStyle = {
color: this.props.content.text_color,
backgroundColor: this.props.content.background_color,
};
const customElement = <em style={{backgroundColor: this.props.content.highlight_color}} />;
return (<SimpleSnippet {...this.props}
className={this.props.content.test}
customElements={{em: customElement}}
textStyle={textStyle}
extraContent={this.renderDonations()} />);
}
}
export const EOYSnippet = props => {
const extendedContent = {
monthly_checkbox_label_text: "Make my donation monthly",
locale: "en-US",
currency_code: "usd",
selected_button: "donation_amount_second",
...props.content,
};
return (<EOYSnippetBase
{...props}
content={extendedContent}
form_method="GET" />);
};

Просмотреть файл

@ -0,0 +1,136 @@
{
"title": "EOYSnippet",
"description": "Fundraising Snippet",
"version": "1.0.0",
"type": "object",
"definitions": {
"plainText": {
"description": "Plain text (no HTML allowed)",
"type": "string"
},
"richText": {
"description": "Text with HTML subset allowed: i, b, u, strong, em, br",
"type": "string"
},
"link_url": {
"description": "Target for links or buttons",
"type": "string",
"format": "uri"
}
},
"properties": {
"donation_form_url": {
"type": "string",
"description": "Url to the donation form."
},
"currency_code": {
"type": "string",
"description": "The code for the currency. Examle gbp, cad, usd."
},
"locale": {
"type": "string",
"description": "String for the locale code."
},
"text": {
"allOf": [
{"$ref": "#/definitions/richText"},
{"description": "Main body text of snippet. HTML subset allowed: i, b, u, strong, em, br"}
]
},
"text_color": {
"type": "string",
"description": "Modify the text message color"
},
"background_color": {
"type": "string",
"description": "Snippet background color."
},
"highlight_color": {
"type": "string",
"description": "Paragraph em highlight color."
},
"donation_amount_first": {
"type": "number",
"description": "First button amount."
},
"donation_amount_second": {
"type": "number",
"description": "Second button amount."
},
"donation_amount_third": {
"type": "number",
"description": "Third button amount."
},
"donation_amount_fourth": {
"type": "number",
"description": "Fourth button amount."
},
"selected_button": {
"type": "string",
"description": "Default donation_amount_second. Donation amount button that's selected by default."
},
"icon": {
"type": "string",
"description": "Snippet icon. 64x64px. SVG or PNG preferred."
},
"title_icon": {
"type": "string",
"description": "Small icon that shows up before the title / text. 16x16px. SVG or PNG preferred. Grayscale."
},
"button_label": {
"allOf": [
{"$ref": "#/definitions/plainText"},
{"description": "Text for a button next to main snippet text that links to button_url. Requires button_url."}
]
},
"button_color": {
"type": "string",
"description": "The text color of the button. Valid CSS color."
},
"button_background_color": {
"type": "string",
"description": "The background color of the button. Valid CSS color."
},
"block_button_text": {
"type": "string",
"description": "Tooltip text used for dismiss button."
},
"monthly_checkbox_label_text": {
"type": "string",
"description": "Label text for monthly checkbox."
},
"test": {
"type": "string",
"description": "Different styles for the snippet. Options are bold and takeover."
},
"do_not_autoblock": {
"type": "boolean",
"description": "Used to prevent blocking the snippet after the CTA (link or button) has been clicked"
},
"links": {
"additionalProperties": {
"url": {
"allOf": [
{"$ref": "#/definitions/link_url"},
{"description": "The url where the link points to."}
]
},
"metric": {
"type": "string",
"description": "Custom event name sent with telemetry event."
},
"args": {
"type": "string",
"description": "Additional parameters for link action, example which specific menu the button should open"
}
}
}
},
"additionalProperties": false,
"required": ["text", "donation_form_url", "donation_amount_first", "donation_amount_second", "donation_amount_third", "donation_amount_fourth", "button_label", "currency_code"],
"dependencies": {
"button_color": ["button_label"],
"button_background_color": ["button_label"]
}
}

Просмотреть файл

@ -0,0 +1,50 @@
.EOYSnippetForm {
margin-top: 12px;
align-self: start;
font-size: 12px;
display: flex;
align-items: center;
.donation-amount,
.donation-form-url {
white-space: nowrap;
font-size: 14px;
padding: 5px 14px;
border-radius: 2px;
}
.donation-amount {
color: $grey-90;
margin-inline-end: 18px;
border: 1px solid $grey-40;
background: $grey-10;
cursor: pointer;
}
input {
&[type='radio'] {
opacity: 0;
margin-inline-end: -18px;
&:checked+.donation-amount {
background: $grey-50;
color: $white;
border: 1px solid $grey-60;
}
}
}
.monthly-checkbox-container {
width: 100%;
}
.donation-form-url {
margin-inline-start: 18px;
background-color: $snippets-donation-button-bg;
border: 0;
color: $white;
align-self: flex-end;
display: flex;
cursor: pointer;
}
}

Просмотреть файл

@ -0,0 +1,27 @@
import React from "react";
import {SubmitFormSnippet} from "../SubmitFormSnippet/SubmitFormSnippet.jsx";
export const FXASignupSnippet = props => {
const userAgent = window.navigator.userAgent.match(/Firefox\/([0-9]+)\./);
const firefox_version = userAgent ? parseInt(userAgent[1], 10) : 0;
const extendedContent = {
form_action: "https://accounts.firefox.com/",
...props.content,
hidden_inputs: {
action: "email",
context: "fx_desktop_v3",
entrypoint: "snippets",
service: "sync",
utm_source: "snippet",
utm_content: firefox_version,
utm_campaign: props.content.utm_campaign,
utm_term: props.content.utm_term,
...props.content.hidden_inputs,
},
};
return (<SubmitFormSnippet
{...props}
content={extendedContent}
form_method="GET" />);
};

Просмотреть файл

@ -0,0 +1,21 @@
import React from "react";
import {SubmitFormSnippet} from "../SubmitFormSnippet/SubmitFormSnippet.jsx";
export const NewsletterSnippet = props => {
const extendedContent = {
form_action: "https://basket.mozilla.org/subscribe.json",
...props.content,
hidden_inputs: {
newsletters: props.content.scene2_newsletter || "mozilla-foundation",
fmt: "H",
lang: "en-US",
source_url: `https://snippets.mozilla.com/show/${props.id}`,
...props.content.hidden_inputs,
},
};
return (<SubmitFormSnippet
{...props}
content={extendedContent}
form_method="POST" />);
};

Просмотреть файл

@ -55,6 +55,7 @@ export class SimpleSnippet extends React.PureComponent {
renderText() {
const {props} = this;
return (<RichText text={props.content.text}
customElements={this.props.customElements}
localization_id="text"
links={props.content.links}
sendClick={props.sendClick} />);
@ -62,11 +63,18 @@ export class SimpleSnippet extends React.PureComponent {
render() {
const {props} = this;
const className = `SimpleSnippet${props.content.tall ? " tall" : ""}`;
return (<SnippetBase {...props} className={className}>
let className = "SimpleSnippet";
if (props.className) {
className += ` ${props.className}`;
}
if (props.content.tall) {
className += " tall";
}
return (<SnippetBase {...props} className={className} textStyle={this.props.textStyle}>
<img src={safeURI(props.content.icon) || DEFAULT_ICON_PATH} className="icon" />
<div>
{this.renderTitleIcon()} {this.renderTitle()} <p className="body">{this.renderText()}</p>
{this.props.extraContent}
</div>
{<div>{this.renderButton()}</div>}
</SnippetBase>);

Просмотреть файл

@ -3,6 +3,51 @@
padding: 27px 0;
}
p em {
color: $grey-90;
font-style: normal;
background: $yellow-50;
}
&.bold,
&.takeover {
.donation-form-url,
.donation-amount {
padding-top: 8px;
padding-bottom: 8px;
}
}
&.bold {
height: 176px;
.body {
font-size: 14px;
line-height: 20px;
margin-bottom: 20px;
}
.icon {
width: 71px;
height: 71px;
}
}
&.takeover {
height: 344px;
.body {
font-size: 16px;
line-height: 24px;
margin-bottom: 35px;
}
.icon {
width: 79px;
height: 79px;
}
}
.title {
display: inline;
font-size: inherit;
@ -29,6 +74,17 @@
margin-inline-end: 20px;
}
&.takeover,
&.bold {
.icon {
margin-inline-end: 20px;
}
}
.icon {
align-self: flex-start;
}
.ASRouterButton {
cursor: pointer;
}

Просмотреть файл

@ -48,7 +48,9 @@ export class SubmitFormSnippet extends React.PureComponent {
if (json && json.status === "ok") {
this.setState({signupSuccess: true, signupSubmitted: true});
this.props.onBlock({preventDismiss: true});
if (!this.props.content.do_not_autoblock) {
this.props.onBlock({preventDismiss: true});
}
this.props.sendUserActionTelemetry({event: "CLICK_BUTTON", value: "subscribe-success", id: "NEWTAB_FOOTER_BAR_CONTENT"});
} else {
console.error("There was a problem submitting the form", json || "[No JSON response]"); // eslint-disable-line no-console
@ -86,7 +88,7 @@ export class SubmitFormSnippet extends React.PureComponent {
<RichText text={content.scene2_disclaimer_html}
localization_id="disclaimer_html"
links={content.links}
autoBlock={false}
doNotAutoBlock={true}
sendClick={this.props.sendClick} />
</p>);
}
@ -102,7 +104,7 @@ export class SubmitFormSnippet extends React.PureComponent {
<span><RichText text={content.scene2_privacy_html}
localization_id="privacy_html"
links={content.links}
autoBlock={false}
doNotAutoBlock={true}
sendClick={this.props.sendClick} />
</span>
</p>

Просмотреть файл

@ -103,6 +103,10 @@
"type": "string",
"description": "(send to device) Image to display above the form. 98x98px. SVG or PNG preferred."
},
"scene2_newsletter": {
"type": "string",
"description": "Newsletter/basket id user is subscribing to. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/. Default 'mozilla-foundation'."
},
"hidden_inputs": {
"type": "object",
"description": "Each entry represents a hidden input, key is used as value for the name property."
@ -137,6 +141,14 @@
"type": "string",
"description": "(send to device) Newsletter/basket id representing the email message to be sent. Must be a value from the 'Slug' column here: https://basket.mozilla.org/news/."
},
"utm_campaign": {
"type": "string",
"description": "(fxa) Value to pass through to GA as utm_campaign."
},
"utm_term": {
"type": "string",
"description": "(fxa) Value to pass through to GA as utm_term."
},
"links": {
"additionalProperties": {
"url": {
@ -153,7 +165,7 @@
}
},
"additionalProperties": false,
"required": ["scene1_text", "form_action", "scene2_text", "hidden_inputs", "error_text", "success_text", "scene1_button_label"],
"required": ["scene1_text", "scene2_text", "scene1_button_label"],
"dependencies": {
"scene1_button_color": ["scene1_button_label"],
"scene1_button_background_color": ["scene1_button_label"]

Просмотреть файл

@ -0,0 +1,14 @@
import {EOYSnippet} from "./EOYSnippet/EOYSnippet";
import {FXASignupSnippet} from "./FXASignupSnippet/FXASignupSnippet";
import {NewsletterSnippet} from "./NewsletterSnippet/NewsletterSnippet";
import {SendToDeviceSnippet} from "./SendToDeviceSnippet/SendToDeviceSnippet";
import {SimpleSnippet} from "./SimpleSnippet/SimpleSnippet";
// Key names matching schema name of templates
export const SnippetsTemplates = {
simple_snippet: SimpleSnippet,
newsletter_snippet: NewsletterSnippet,
fxa_signup_snippet: FXASignupSnippet,
send_to_device_snippet: SendToDeviceSnippet,
eoy_snippet: EOYSnippet,
};

Просмотреть файл

@ -5,8 +5,10 @@ export class ASRouterAdmin extends React.PureComponent {
constructor(props) {
super(props);
this.onMessage = this.onMessage.bind(this);
this.handleEnabledToggle = this.handleEnabledToggle.bind(this);
this.onChangeMessageFilter = this.onChangeMessageFilter.bind(this);
this.findOtherBundledMessagesOfSameTemplate = this.findOtherBundledMessagesOfSameTemplate.bind(this);
this.state = {};
this.state = {messageFilter: "all"};
}
onMessage({data: action}) {
@ -55,6 +57,10 @@ export class ASRouterAdmin extends React.PureComponent {
ASRouterUtils.sendMessage({type: "EXPIRE_QUERY_CACHE"});
}
resetPref() {
ASRouterUtils.sendMessage({type: "RESET_PROVIDER_PREF"});
}
renderMessageItem(msg) {
const isCurrent = msg.id === this.state.lastMessageId;
const isBlocked = this.state.messageBlockList.includes(msg.id);
@ -81,34 +87,61 @@ export class ASRouterAdmin extends React.PureComponent {
if (!this.state.messages) {
return null;
}
const messagesToShow = this.state.messageFilter === "all" ? this.state.messages : this.state.messages.filter(message => message.provider === this.state.messageFilter);
return (<table><tbody>
{this.state.messages.map(msg => this.renderMessageItem(msg))}
{messagesToShow.map(msg => this.renderMessageItem(msg))}
</tbody></table>);
}
onChangeMessageFilter(event) {
this.setState({messageFilter: event.target.value});
}
renderMessageFilter() {
if (!this.state.providers) {
return null;
}
return (<p>Show messages from <select value={this.state.messageFilter} onChange={this.onChangeMessageFilter}>
<option value="all">all providers</option>
{this.state.providers.map(provider => (<option key={provider.id} value={provider.id}>{provider.id}</option>))}
</select></p>);
}
renderTableHead() {
return (<thead>
<tr className="message-item">
<td>id</td>
<td>enabled</td>
<td>source</td>
<td>last updated</td>
</tr>
</thead>);
}
handleEnabledToggle(event) {
const action = {type: event.target.checked ? "ENABLE_PROVIDER" : "DISABLE_PROVIDER", data: event.target.name};
ASRouterUtils.sendMessage(action);
this.setState({messageFilter: "all"});
}
renderProviders() {
const providersConfig = this.state.providerPrefs;
const providerInfo = this.state.providers;
return (<table>{this.renderTableHead()}<tbody>
{this.state.providers.map((provider, i) => {
{providersConfig.map((provider, i) => {
const isTestProvider = provider.id === "snippets_local_testing";
const info = providerInfo.find(p => p.id === provider.id) || {};
let label = "(local)";
if (provider.type === "remote") {
label = <a target="_blank" href={provider.url}>{provider.url}</a>;
label = <a target="_blank" href={info.url}>{info.url}</a>;
} else if (provider.type === "remote-settings") {
label = `${provider.bucket} (Remote Settings)`;
}
return (<tr className="message-item" key={i}>
<td>{provider.id}</td>
<td>{isTestProvider ? null : <input type="checkbox" name={provider.id} checked={provider.enabled} onChange={this.handleEnabledToggle} />}</td>
<td>{label}</td>
<td>{provider.lastUpdated ? new Date(provider.lastUpdated).toString() : ""}</td>
<td style={{whiteSpace: "nowrap"}}>{info.lastUpdated ? new Date(info.lastUpdated).toLocaleString() : ""}</td>
</tr>);
})}
</tbody></table>);
@ -120,8 +153,10 @@ export class ASRouterAdmin extends React.PureComponent {
<h2>Targeting Utilities</h2>
<button className="button" onClick={this.expireCache}>Expire Cache</button> (This expires the cache in ASR Targeting for bookmarks and top sites)
<h2>Message Providers</h2>
<button className="button" onClick={this.resetPref}>Restore defaults</button>
{this.state.providers ? this.renderProviders() : null}
<h2>Messages</h2>
{this.renderMessageFilter()}
{this.renderMessages()}
</div>);
}

Просмотреть файл

@ -82,8 +82,11 @@ export class _Base extends React.PureComponent {
const {initialized} = App;
const prefs = props.Prefs.values;
if (prefs["asrouter.devtoolsEnabled"] && window.location.hash === "#asrouter") {
return (<ASRouterAdmin />);
if (prefs["asrouter.devtoolsEnabled"]) {
if (window.location.hash === "#asrouter") {
return (<ASRouterAdmin />);
}
console.log("ASRouter devtools enabled. To access visit %cabout:newtab#asrouter", "font-weight: bold"); // eslint-disable-line no-console
}
if (!props.isPrerendered && !initialized) {

Просмотреть файл

@ -152,3 +152,4 @@ input {
@import '../asrouter/templates/SimpleSnippet/SimpleSnippet';
@import '../asrouter/templates/SubmitFormSnippet/SubmitFormSnippet';
@import '../asrouter/templates/OnboardingMessage/OnboardingMessage';
@import '../asrouter/templates/EOYSnippet/EOYSnippet';

Просмотреть файл

@ -131,6 +131,7 @@ $error-fallback-line-height: 1.5;
$image-path: '../data/content/assets/';
$snippets-container-height: 120px;
$snippets-donation-button-bg: #0C99D5;
$textbox-shadow-size: 4px;

Просмотреть файл

@ -1931,7 +1931,8 @@ a.firstrun-link {
align-items: center; }
.SnippetBaseContainer a {
cursor: pointer;
color: var(--newtab-link-primary-color); }
color: var(--newtab-link-primary-color);
text-decoration: underline; }
[lwt-newtab-brighttext] .SnippetBaseContainer a {
font-weight: bold; }
.SnippetBaseContainer .innerWrapper {
@ -2069,6 +2070,37 @@ a.firstrun-link {
.SimpleSnippet.tall {
padding: 27px 0; }
.SimpleSnippet p em {
color: #0C0C0D;
font-style: normal;
background: #FFE900; }
.SimpleSnippet.bold .donation-form-url,
.SimpleSnippet.bold .donation-amount, .SimpleSnippet.takeover .donation-form-url,
.SimpleSnippet.takeover .donation-amount {
padding-top: 8px;
padding-bottom: 8px; }
.SimpleSnippet.bold {
height: 176px; }
.SimpleSnippet.bold .body {
font-size: 14px;
line-height: 20px;
margin-bottom: 20px; }
.SimpleSnippet.bold .icon {
width: 71px;
height: 71px; }
.SimpleSnippet.takeover {
height: 344px; }
.SimpleSnippet.takeover .body {
font-size: 16px;
line-height: 24px;
margin-bottom: 35px; }
.SimpleSnippet.takeover .icon {
width: 79px;
height: 79px; }
.SimpleSnippet .title {
display: inline;
font-size: inherit;
@ -2091,6 +2123,12 @@ a.firstrun-link {
.SimpleSnippet.tall .icon {
margin-inline-end: 20px; }
.SimpleSnippet.takeover .icon, .SimpleSnippet.bold .icon {
margin-inline-end: 20px; }
.SimpleSnippet .icon {
align-self: flex-start; }
.SimpleSnippet .ASRouterButton {
cursor: pointer; }
@ -2273,4 +2311,40 @@ a.firstrun-link {
.onboardingMessage:last-child::before {
content: none; }
.EOYSnippetForm {
margin-top: 12px;
align-self: start;
font-size: 12px;
display: flex;
align-items: center; }
.EOYSnippetForm .donation-amount,
.EOYSnippetForm .donation-form-url {
white-space: nowrap;
font-size: 14px;
padding: 5px 14px;
border-radius: 2px; }
.EOYSnippetForm .donation-amount {
color: #0C0C0D;
margin-inline-end: 18px;
border: 1px solid #B1B1B3;
background: #F9F9FA;
cursor: pointer; }
.EOYSnippetForm input[type='radio'] {
opacity: 0;
margin-inline-end: -18px; }
.EOYSnippetForm input[type='radio']:checked + .donation-amount {
background: #737373;
color: #FFF;
border: 1px solid #4A4A4F; }
.EOYSnippetForm .monthly-checkbox-container {
width: 100%; }
.EOYSnippetForm .donation-form-url {
margin-inline-start: 18px;
background-color: #0C99D5;
border: 0;
color: #FFF;
align-self: flex-end;
display: flex;
cursor: pointer; }
/*# sourceMappingURL=activity-stream-linux.css.map */

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -1934,7 +1934,8 @@ a.firstrun-link {
align-items: center; }
.SnippetBaseContainer a {
cursor: pointer;
color: var(--newtab-link-primary-color); }
color: var(--newtab-link-primary-color);
text-decoration: underline; }
[lwt-newtab-brighttext] .SnippetBaseContainer a {
font-weight: bold; }
.SnippetBaseContainer .innerWrapper {
@ -2072,6 +2073,37 @@ a.firstrun-link {
.SimpleSnippet.tall {
padding: 27px 0; }
.SimpleSnippet p em {
color: #0C0C0D;
font-style: normal;
background: #FFE900; }
.SimpleSnippet.bold .donation-form-url,
.SimpleSnippet.bold .donation-amount, .SimpleSnippet.takeover .donation-form-url,
.SimpleSnippet.takeover .donation-amount {
padding-top: 8px;
padding-bottom: 8px; }
.SimpleSnippet.bold {
height: 176px; }
.SimpleSnippet.bold .body {
font-size: 14px;
line-height: 20px;
margin-bottom: 20px; }
.SimpleSnippet.bold .icon {
width: 71px;
height: 71px; }
.SimpleSnippet.takeover {
height: 344px; }
.SimpleSnippet.takeover .body {
font-size: 16px;
line-height: 24px;
margin-bottom: 35px; }
.SimpleSnippet.takeover .icon {
width: 79px;
height: 79px; }
.SimpleSnippet .title {
display: inline;
font-size: inherit;
@ -2094,6 +2126,12 @@ a.firstrun-link {
.SimpleSnippet.tall .icon {
margin-inline-end: 20px; }
.SimpleSnippet.takeover .icon, .SimpleSnippet.bold .icon {
margin-inline-end: 20px; }
.SimpleSnippet .icon {
align-self: flex-start; }
.SimpleSnippet .ASRouterButton {
cursor: pointer; }
@ -2276,4 +2314,40 @@ a.firstrun-link {
.onboardingMessage:last-child::before {
content: none; }
.EOYSnippetForm {
margin-top: 12px;
align-self: start;
font-size: 12px;
display: flex;
align-items: center; }
.EOYSnippetForm .donation-amount,
.EOYSnippetForm .donation-form-url {
white-space: nowrap;
font-size: 14px;
padding: 5px 14px;
border-radius: 2px; }
.EOYSnippetForm .donation-amount {
color: #0C0C0D;
margin-inline-end: 18px;
border: 1px solid #B1B1B3;
background: #F9F9FA;
cursor: pointer; }
.EOYSnippetForm input[type='radio'] {
opacity: 0;
margin-inline-end: -18px; }
.EOYSnippetForm input[type='radio']:checked + .donation-amount {
background: #737373;
color: #FFF;
border: 1px solid #4A4A4F; }
.EOYSnippetForm .monthly-checkbox-container {
width: 100%; }
.EOYSnippetForm .donation-form-url {
margin-inline-start: 18px;
background-color: #0C99D5;
border: 0;
color: #FFF;
align-self: flex-end;
display: flex;
cursor: pointer; }
/*# sourceMappingURL=activity-stream-mac.css.map */

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -1931,7 +1931,8 @@ a.firstrun-link {
align-items: center; }
.SnippetBaseContainer a {
cursor: pointer;
color: var(--newtab-link-primary-color); }
color: var(--newtab-link-primary-color);
text-decoration: underline; }
[lwt-newtab-brighttext] .SnippetBaseContainer a {
font-weight: bold; }
.SnippetBaseContainer .innerWrapper {
@ -2069,6 +2070,37 @@ a.firstrun-link {
.SimpleSnippet.tall {
padding: 27px 0; }
.SimpleSnippet p em {
color: #0C0C0D;
font-style: normal;
background: #FFE900; }
.SimpleSnippet.bold .donation-form-url,
.SimpleSnippet.bold .donation-amount, .SimpleSnippet.takeover .donation-form-url,
.SimpleSnippet.takeover .donation-amount {
padding-top: 8px;
padding-bottom: 8px; }
.SimpleSnippet.bold {
height: 176px; }
.SimpleSnippet.bold .body {
font-size: 14px;
line-height: 20px;
margin-bottom: 20px; }
.SimpleSnippet.bold .icon {
width: 71px;
height: 71px; }
.SimpleSnippet.takeover {
height: 344px; }
.SimpleSnippet.takeover .body {
font-size: 16px;
line-height: 24px;
margin-bottom: 35px; }
.SimpleSnippet.takeover .icon {
width: 79px;
height: 79px; }
.SimpleSnippet .title {
display: inline;
font-size: inherit;
@ -2091,6 +2123,12 @@ a.firstrun-link {
.SimpleSnippet.tall .icon {
margin-inline-end: 20px; }
.SimpleSnippet.takeover .icon, .SimpleSnippet.bold .icon {
margin-inline-end: 20px; }
.SimpleSnippet .icon {
align-self: flex-start; }
.SimpleSnippet .ASRouterButton {
cursor: pointer; }
@ -2273,4 +2311,40 @@ a.firstrun-link {
.onboardingMessage:last-child::before {
content: none; }
.EOYSnippetForm {
margin-top: 12px;
align-self: start;
font-size: 12px;
display: flex;
align-items: center; }
.EOYSnippetForm .donation-amount,
.EOYSnippetForm .donation-form-url {
white-space: nowrap;
font-size: 14px;
padding: 5px 14px;
border-radius: 2px; }
.EOYSnippetForm .donation-amount {
color: #0C0C0D;
margin-inline-end: 18px;
border: 1px solid #B1B1B3;
background: #F9F9FA;
cursor: pointer; }
.EOYSnippetForm input[type='radio'] {
opacity: 0;
margin-inline-end: -18px; }
.EOYSnippetForm input[type='radio']:checked + .donation-amount {
background: #737373;
color: #FFF;
border: 1px solid #4A4A4F; }
.EOYSnippetForm .monthly-checkbox-container {
width: 100%; }
.EOYSnippetForm .donation-form-url {
margin-inline-start: 18px;
background-color: #0C99D5;
border: 0;
color: #FFF;
align-self: flex-end;
display: flex;
cursor: pointer; }
/*# sourceMappingURL=activity-stream-windows.css.map */

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -497,7 +497,13 @@ class _ASRouter {
_updateAdminState(target) {
const channel = target || this.messageChannel;
channel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "ADMIN_SET_STATE", data: this.state});
channel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
type: "ADMIN_SET_STATE",
data: {
...this.state,
providerPrefs: ASRouterPreferences.providers,
},
});
}
_handleTargetingError(type, error, message) {
@ -996,6 +1002,15 @@ class _ASRouter {
case "EXPIRE_QUERY_CACHE":
QueryCache.expireAll();
break;
case "ENABLE_PROVIDER":
ASRouterPreferences.enableOrDisableProvider(action.data, true);
break;
case "DISABLE_PROVIDER":
ASRouterPreferences.enableOrDisableProvider(action.data, false);
break;
case "RESET_PROVIDER_PREF":
ASRouterPreferences.resetProviderPref();
break;
}
}
}

Просмотреть файл

@ -18,6 +18,7 @@ const DEFAULT_STATE = {
const USER_PREFERENCES = {
snippets: "browser.newtabpage.activity-stream.feeds.snippets",
cfr: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr",
};
const TEST_PROVIDER = {
@ -33,16 +34,19 @@ class _ASRouterPreferences {
this._callbacks = new Set();
}
_getProviderConfig() {
try {
return JSON.parse(Services.prefs.getStringPref(this._providerPref, ""));
} catch (e) {
Cu.reportError(`Could not parse ASRouter preference. Try resetting ${this._providerPref} in about:config.`);
}
return null;
}
get providers() {
if (!this._initialized || this._providers === null) {
let providers;
try {
const parsed = JSON.parse(Services.prefs.getStringPref(this._providerPref, ""));
providers = parsed.map(provider => Object.freeze(provider));
} catch (e) {
Cu.reportError("Problem parsing JSON message provider pref for ASRouter");
providers = [];
}
const config = this._getProviderConfig() || [];
const providers = config.map(provider => Object.freeze(provider));
if (this.devtoolsEnabled) {
providers.unshift(TEST_PROVIDER);
}
@ -52,6 +56,33 @@ class _ASRouterPreferences {
return this._providers;
}
enableOrDisableProvider(id, value) {
const providers = this._getProviderConfig();
if (!providers) {
Cu.reportError(`Cannot enable/disable providers if ${this._providerPref} is unparseable.`);
return;
}
if (!providers.find(p => p.id === id)) {
Cu.reportError(`Cannot set enabled state for '${id}' because it does not exist in ${this._providerPref}`);
return;
}
const newConfig = providers.map(provider => {
if (provider.id === id) {
return {...provider, enabled: value};
}
return provider;
});
Services.prefs.setStringPref(this._providerPref, JSON.stringify(newConfig));
}
resetProviderPref() {
Services.prefs.clearUserPref(this._providerPref);
for (const id of Object.keys(USER_PREFERENCES)) {
Services.prefs.clearUserPref(USER_PREFERENCES[id]);
}
}
get devtoolsEnabled() {
if (!this._initialized || this._devtoolsEnabled === null) {
this._devtoolsEnabled = Services.prefs.getBoolPref(this._devtoolsPref, false);

Просмотреть файл

@ -15,6 +15,8 @@ ChromeUtils.defineModuleGetter(this, "TelemetryEnvironment",
"resource://gre/modules/TelemetryEnvironment.jsm");
ChromeUtils.defineModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
ChromeUtils.defineModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
const FXA_USERNAME_PREF = "services.sync.username";
const SEARCH_REGION_PREF = "browser.search.region";
@ -246,6 +248,13 @@ const TargetingGetters = {
}
)));
},
get pinnedSites() {
return NewTabUtils.pinnedLinks.links.map(site => ({
url: site.url,
host: (new URL(site.url)).hostname,
searchTopSite: site.searchTopSite,
}));
},
get providerCohorts() {
return ASRouterPreferences.providers.reduce((prev, current) => {
prev[current.id] = current.cohort || "";

Просмотреть файл

@ -198,6 +198,10 @@ const PREFS_CONFIG = new Map([
title: "Are the asrouter devtools enabled?",
value: false,
}],
["asrouter.userprefs.cfr", {
title: "Does the user allow CFR recommendations?",
value: true,
}],
["asrouter.messageProviders", {
title: "Configuration for ASRouter message providers",

Просмотреть файл

@ -16,6 +16,19 @@ const MESSAGES = () => ([
"block_button_text": "Block",
},
},
{
"id": "SIMPLE_TEST_TALL",
"template": "simple_snippet",
"content": {
"icon": TEST_ICON,
"text": "<syncLink>Sync it, link it, take it with you</syncLink>. All this and more with a Firefox Account.",
"links": {"syncLink": {"url": "https://www.mozilla.org/en-US/firefox/accounts"}},
"button_label": "Get one now!",
"button_url": "https://www.mozilla.org/en-US/firefox/accounts",
"block_button_text": "Block",
"tall": true,
},
},
{
"id": "SIMPLE_TEST_BUTTON_URL_1",
"template": "simple_snippet",
@ -84,11 +97,6 @@ const MESSAGES = () => ([
"scene2_button_label": "Continue",
"scene2_dismiss_button_text": "Dismiss",
"form_action": "https://basket.mozilla.org/subscribe.json",
// TODO: This should not be required
"success_text": "Check your inbox for the confirmation!",
"error_text": "Error!",
"hidden_inputs": {},
},
},
{
@ -121,10 +129,59 @@ const MESSAGES = () => ([
success_title: "Your download link was sent.",
success_text: "Check your device for the email message!",
links: {"privacyLink": {"url": "https://www.mozilla.org/privacy/websites/?sample_rate=0.001&snippet_name=7894"}},
// TODO: Not actually defined in the send to device schema
form_action: "https://basket.mozilla.org/subscribe.json",
hidden_inputs: {},
},
},
{
"id": "EOY_TEST_1",
"template": "eoy_snippet",
"content": {
"highlight_color": "#f05",
"selected_button": "donation_amount_first",
"icon": TEST_ICON,
"button_label": "Donate",
"monthly_checkbox_label_text": "Make my donation monthly",
"currency_code": "usd",
"donation_amount_first": 50,
"donation_amount_second": 25,
"donation_amount_third": 10,
"donation_amount_fourth": 5,
"donation_form_url": "https://donate.mozilla.org",
"text": "Big corporations want to restrict how we access the web. Fake news is making it harder for us to find the truth. Online bullies are silencing inspired voices. The <em>not-for-profit Mozilla Foundation</em> fights for a healthy internet with programs like our Tech Policy Fellowships and Internet Health Report; <b>will you donate today</b>?",
},
},
{
"id": "EOY_BOLD_TEST_1",
"template": "eoy_snippet",
"content": {
"icon": TEST_ICON,
"selected_button": "donation_amount_second",
"button_label": "Donate",
"monthly_checkbox_label_text": "Make my donation monthly",
"currency_code": "usd",
"donation_amount_first": 50,
"donation_amount_second": 25,
"donation_amount_third": 10,
"donation_amount_fourth": 5,
"donation_form_url": "https://donate.mozilla.org",
"text": "Big corporations want to restrict how we access the web. Fake news is making it harder for us to find the truth. Online bullies are silencing inspired voices. The <em>not-for-profit Mozilla Foundation</em> fights for a healthy internet with programs like our Tech Policy Fellowships and Internet Health Report; <b>will you donate today</b>?",
"test": "bold",
},
},
{
"id": "EOY_TAKEOVER_TEST_1",
"template": "eoy_snippet",
"content": {
"icon": TEST_ICON,
"button_label": "Donate",
"monthly_checkbox_label_text": "Make my donation monthly",
"currency_code": "usd",
"donation_amount_first": 50,
"donation_amount_second": 25,
"donation_amount_third": 10,
"donation_amount_fourth": 5,
"donation_form_url": "https://donate.mozilla.org",
"text": "Big corporations want to restrict how we access the web. Fake news is making it harder for us to find the truth. Online bullies are silencing inspired voices. The <em>not-for-profit Mozilla Foundation</em> fights for a healthy internet with programs like our Tech Policy Fellowships and Internet Health Report; <b>will you donate today</b>?",
"test": "takeover",
},
},
]);

Просмотреть файл

@ -492,6 +492,7 @@ this.TelemetryFeed = class TelemetryFeed {
async sendPageTakeoverData() {
if (this.telemetryEnabled) {
const value = {};
let page;
// Check whether or not about:home and about:newtab are set to a custom URL.
// If so, classify them.
@ -499,21 +500,25 @@ this.TelemetryFeed = class TelemetryFeed {
aboutNewTabService.overridden &&
!aboutNewTabService.newTabURL.startsWith("moz-extension://")) {
value.newtab_url_category = await this._classifySite(aboutNewTabService.newTabURL);
page = "about:newtab";
}
const homePageURL = HomePage.get();
if (!["about:home", "about:blank"].includes(homePageURL) &&
!homePageURL.startsWith("moz-extension://")) {
value.home_url_category = await this._classifySite(homePageURL);
page = page ? "both" : "about:home";
}
if (value.newtab_url_category || value.home_url_category) {
if (page) {
const event = Object.assign(
this.createPing(),
{
action: "activity_stream_user_event",
event: "PAGE_TAKEOVER_DATA",
value,
page,
session_id: "n/a",
},
);
this.sendEvent(event);

Просмотреть файл

@ -143,9 +143,9 @@ pocket_read_more=Популярни теми:
# LOCALIZATION NOTE (pocket_read_even_more): This is shown as a link at the
# end of the list of popular topic links.
pocket_read_even_more=Повече публикации
pocket_more_reccommendations=Повече препоръчани
pocket_learn_more=Научете повече
pocket_how_it_works=Как работи
pocket_cta_button=Вземете Pocket
pocket_cta_text=Запазете статиите, които харесвате в Pocket и заредете ума си с увлекателни четива.
@ -196,7 +196,6 @@ firstrun_form_header=Въведете своята ел. поща,
firstrun_form_sub_header=за да продължите към Firefox Sync
firstrun_email_input_placeholder=адрес на електронна поща
firstrun_invalid_input=Необходим е валиден адрес на ел. поща
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and

Просмотреть файл

@ -1,33 +1,18 @@
newtab_page_title=Nuova scheda
# 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/.
newtab_page_title=Nuova scheda
header_top_sites=Siti principali
header_highlights=In evidenza
# LOCALIZATION NOTE(header_recommended_by): This is followed by the name
# of the corresponding content provider.
header_recommended_by=Consigliati da {provider}
# LOCALIZATION NOTE(context_menu_button_sr): This is for screen readers when
# the context menu button is focused/active. Title is the label or hostname of
# the site.
context_menu_button_sr=Apri menu contestuale per {title}
# LOCALIZATION NOTE(section_context_menu_button_sr): This is for screen readers when
# the section edit context menu button is focused/active.
section_context_menu_button_sr=Apri il menu contestuale per la sezione
# LOCALIZATION NOTE (type_label_*): These labels are associated to pages to give
# context on how the element is related to the user, e.g. type indicates that
# the page is bookmarked, or is currently open on another device
type_label_visited=Visitato
type_label_bookmarked=Nei segnalibri
type_label_recommended=Di tendenza
type_label_pocket=Salvato in Pocket
type_label_downloaded=Scaricata
# LOCALIZATION NOTE (menu_action_*): These strings are displayed in a context
# menu and are meant as a call to action for a given page.
# LOCALIZATION NOTE (menu_action_bookmark): Bookmark is a verb, as in "Add to
# bookmarks"
menu_action_bookmark=Aggiungi ai segnalibri
menu_action_remove_bookmark=Elimina segnalibro
menu_action_open_new_window=Apri in una nuova finestra
@ -37,63 +22,26 @@ menu_action_delete=Elimina dalla cronologia
menu_action_pin=Appunta
menu_action_unpin=Rilascia
confirm_history_delete_p1=Eliminare tutte le occorrenze di questa pagina dalla cronologia?
# LOCALIZATION NOTE (confirm_history_delete_notice_p2): this string is displayed in
# the same dialog as confirm_history_delete_p1. "This action" refers to deleting a
# page from history.
confirm_history_delete_notice_p2=Questa operazione non può essere annullata.
menu_action_save_to_pocket=Salva in Pocket
menu_action_delete_pocket=Elimina da Pocket
menu_action_archive_pocket=Archivia in Pocket
# LOCALIZATION NOTE (menu_action_show_file_*): These are platform specific strings
# found in the context menu of an item that has been downloaded. The intention behind
# "this action" is that it will show where the downloaded file exists on the file system
# for each operating system.
menu_action_show_file_mac_os=Mostra nel Finder
menu_action_show_file_windows=Apri cartella di destinazione
menu_action_show_file_linux=Apri cartella di destinazione
menu_action_show_file_default=Mostra file
menu_action_open_file=Apri file
# LOCALIZATION NOTE (menu_action_copy_download_link, menu_action_go_to_download_page):
# "Download" here, in both cases, is not a verb, it is a noun. As in, "Copy the
# link that belongs to this downloaded item"
menu_action_copy_download_link=Copia indirizzo di origine
menu_action_go_to_download_page=Vai alla pagina di download
menu_action_remove_download=Elimina dalla cronologia
# LOCALIZATION NOTE (search_button): This is screenreader only text for the
# search button.
search_button=Cerca
# LOCALIZATION NOTE (search_header): Displayed at the top of the panel
# showing search suggestions. {search_engine_name} is replaced with the name of
# the current default search engine. e.g. 'Google Search'
search_header=Ricerca {search_engine_name}
# LOCALIZATION NOTE (search_web_placeholder): This is shown in the searchbox when
# the user hasn't typed anything yet.
search_web_placeholder=Cerca sul Web
# LOCALIZATION NOTE (section_disclaimer_topstories): This is shown below
# the topstories section title to provide additional information about
# how the stories are selected.
section_disclaimer_topstories=Le storie più interessanti del Web, selezionate in base alle tue letture. Direttamente da Pocket, ora parte del gruppo Mozilla.
section_disclaimer_topstories_linktext=Scopri come funziona.
# LOCALIZATION NOTE (section_disclaimer_topstories_buttontext): The text of
# the button used to acknowledge, and hide this disclaimer in the future.
section_disclaimer_topstories_buttontext=Ho capito.
# LOCALIZATION NOTE (prefs_*, settings_*): These are shown in about:preferences
# for a "Firefox Home" section. "Firefox" should be treated as a brand and kept
# in English, while "Home" should be localized matching the about:preferences
# sidebar mozilla-central string for the panel that has preferences related to
# what is shown for the homepage, new windows, and new tabs.
prefs_home_header=Pagina iniziale di Firefox
prefs_home_description=Scegli i contenuti da visualizzare nella pagina iniziale di Firefox.
# LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
# plural forms used in a drop down of multiple row options (1 row, 2 rows).
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
prefs_section_rows_option={num} riga;{num} righe
prefs_search_header=Ricerca sul Web
prefs_topsites_description=I siti più visitati
@ -109,18 +57,9 @@ settings_pane_button_label=Personalizza la pagina Nuova scheda
settings_pane_topsites_header=Siti principali
settings_pane_highlights_header=In evidenza
settings_pane_highlights_options_bookmarks=Segnalibri
# LOCALIZATION NOTE(settings_pane_snippets_header): For the "Snippets" feature
# traditionally on about:home. Alternative translation options: "Small Note" or
# something that expresses the idea of "a small message, shortened from
# something else, and non-essential but also not entirely trivial and useless."
settings_pane_snippets_header=Snippet
# LOCALIZATION NOTE (edit_topsites_*): This is shown in the Edit Top Sites modal
# dialog.
edit_topsites_button_text=Modifica
edit_topsites_edit_button=Modifica questo sito
# LOCALIZATION NOTE (topsites_form_*): This is shown in the New/Edit Topsite modal.
topsites_form_add_header=Nuovi sito principale
topsites_form_edit_header=Modifica sito principale
topsites_form_title_label=Titolo
@ -129,49 +68,26 @@ topsites_form_url_label=URL
topsites_form_image_url_label=Indirizzo immagine personalizzata
topsites_form_url_placeholder=Digitare o incollare un URL
topsites_form_use_image_link=Utilizza unimmagine personalizzata…
# LOCALIZATION NOTE (topsites_form_*_button): These are verbs/actions.
topsites_form_preview_button=Anteprima
topsites_form_add_button=Aggiungi
topsites_form_save_button=Salva
topsites_form_cancel_button=Annulla
topsites_form_url_validation=È necessario fornire un URL valido
topsites_form_image_validation=Errore durante il caricamento dellimmagine. Prova con un altro indirizzo.
# LOCALIZATION NOTE (pocket_read_more): This is shown at the bottom of the
# trending stories section and precedes a list of links to popular topics.
pocket_read_more=Argomenti popolari:
# LOCALIZATION NOTE (pocket_read_even_more): This is shown as a link at the
# end of the list of popular topic links.
pocket_read_even_more=Visualizza altre storie
pocket_more_reccommendations = Altri suggerimenti
pocket_learn_more = Ulteriori informazioni
pocket_how_it_works = Come funziona
pocket_cta_button = Ottieni Pocket
pocket_cta_text = Salva le storie che ami in Pocket e nutri la tua mente con letture appassionanti.
highlights_empty_state=Inizia a navigare e, in questa sezione, verranno visualizzati articoli, video e altre pagine visitate di recente o aggiunte ai segnalibri.
# LOCALIZATION NOTE (topstories_empty_state): When there are no recommendations,
# in the space that would have shown a few stories, this is shown instead.
# {provider} is replaced by the name of the content provider for this section.
topstories_empty_state=Non cè altro. Controlla più tardi per altre storie da {provider}. Non vuoi aspettare? Seleziona un argomento tra quelli più popolari per scoprire altre notizie interessanti dal Web.
# LOCALIZATION NOTE (manual_migration_explanation2): This message is shown to encourage users to
# import their browser profile from another browser they might be using.
manual_migration_explanation2=Prova Firefox con i segnalibri, la cronologia e le password di un altro browser.
# LOCALIZATION NOTE (manual_migration_cancel_button): This message is shown on a button that cancels the
# process of importing another browsers profile into Firefox.
manual_migration_cancel_button=No grazie
# LOCALIZATION NOTE (manual_migration_import_button): This message is shown on a button that starts the process
# of importing another browsers profile profile into Firefox.
manual_migration_import_button=Importa adesso
# LOCALIZATION NOTE (error_fallback_default_*): This message and suggested
# action link are shown in each section of UI that fails to render
error_fallback_default_info=Oops, qualcosa è andato storto durante il tentativo di caricare questo contenuto.
error_fallback_default_refresh_suggestion=Aggiornare la pagina per riprovare.
# LOCALIZATION NOTE (section_menu_action_*). These strings are displayed in the section
# context menu and are meant as a call to action for the given section.
section_menu_action_remove_section=Rimuovi sezione
section_menu_action_collapse_section=Comprimi sezione
section_menu_action_expand_section=Espandi sezione
@ -182,27 +98,16 @@ section_menu_action_add_search_engine=Aggiungi motore di ricerca
section_menu_action_move_up=Sposta su
section_menu_action_move_down=Sposta giù
section_menu_action_privacy_notice=Informativa sulla privacy
# LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
# firstrun of the browser, they give an introduction to Firefox and Sync.
firstrun_title=Porta Firefox con te
firstrun_content=Ritrova segnalibri, cronologia, password e altre impostazioni su tutti i tuoi dispositivi.
firstrun_learn_more_link=Scopri di più sullaccount Firefox
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
# firstrun_form_header is displayed more boldly as the call to action.
firstrun_form_header=Inserisci email
firstrun_form_sub_header=per utilizzare Firefox Sync.
firstrun_email_input_placeholder=Email
firstrun_invalid_input=Inserire un indirizzo email valido
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=Proseguendo, accetto le {terms} e l{privacy}.
firstrun_terms_of_service=condizioni di utilizzo del servizio
firstrun_privacy_notice=informativa sulla privacy
firstrun_continue_to_login=Continua
firstrun_skip_login=Ignora questo passaggio
context_menu_title=Apri menu

Просмотреть файл

@ -143,9 +143,9 @@ pocket_read_more=Populārās tēmas:
# LOCALIZATION NOTE (pocket_read_even_more): This is shown as a link at the
# end of the list of popular topic links.
pocket_read_even_more=Parādīt vairāk lapas
pocket_more_reccommendations=Vairāk ieteikumu
pocket_learn_more=Uzzināt vairāk
pocket_how_it_works=Kā tas strādā
pocket_cta_button=Izmēģiniet Pocket
pocket_cta_text=Saglabājiet interesantus stāstus Pocket un barojiet savu prātu ar interesantu lasāmvielu.
@ -196,7 +196,6 @@ firstrun_form_header=Ievadiet savu epastu
firstrun_form_sub_header=lai turpinātu Firefox Sync.
firstrun_email_input_placeholder=Epasts
firstrun_invalid_input=Nepieciešams derīgs epasts
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and

Просмотреть файл

@ -206,3 +206,6 @@ firstrun_privacy_notice=Privacyverklaring
firstrun_continue_to_login=Doorgaan
firstrun_skip_login=Deze stap overslaan
# LOCALIZATION NOTE (context_menu_title): Action tooltip to open a context menu
context_menu_title=Menu openen

Просмотреть файл

@ -143,9 +143,9 @@ pocket_read_more=Temas populars:
# LOCALIZATION NOTE (pocket_read_even_more): This is shown as a link at the
# end of the list of popular topic links.
pocket_read_even_more=Mussar dapli artitgels
pocket_more_reccommendations=Dapli propostas
pocket_learn_more=Ulteriuras infurmaziuns
pocket_how_it_works=Co ch'i funcziuna
pocket_cta_button=Obtegnair Pocket
pocket_cta_text=Memorisescha ils artitgels che ta plaschan en Pocket e procura per inspiraziun cuntinuanta cun lectura fascinanta.
@ -196,7 +196,6 @@ firstrun_form_header=Endatescha tia adressa dad e-mail
firstrun_form_sub_header=per cuntinuar cun Firefox Sync.
firstrun_email_input_placeholder=E-mail
firstrun_invalid_input=Adressa dad e-mail valida è obligatorica
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and

Просмотреть файл

@ -26,7 +26,7 @@ type_label_downloaded=ڈاؤن لوڈ شدہ
# menu and are meant as a call to action for a given page.
# LOCALIZATION NOTE (menu_action_bookmark): Bookmark is a verb, as in "Add to
# bookmarks"
menu_action_bookmark=نشانی
menu_action_bookmark=بک مارک
menu_action_remove_bookmark=نشانى ہٹائيں
menu_action_open_new_window=نئے دریچے میں کھولیں
menu_action_open_private_window=نئی نجی دریچے میں کھولیں
@ -100,7 +100,7 @@ prefs_snippets_description=Mozilla اورFirefox کی جانب سے تازہ ک
settings_pane_button_label=اپنے نئے ٹیب کہ صفحہ کی تخصیص کریں
settings_pane_topsites_header=بہترین سائٹیں
settings_pane_highlights_header=شہ سرخياں
settings_pane_highlights_options_bookmarks=نشانیاں
settings_pane_highlights_options_bookmarks=بک مارک
# LOCALIZATION NOTE(settings_pane_snippets_header): For the "Snippets" feature
# traditionally on about:home. Alternative translation options: "Small Note" or
# something that expresses the idea of "a small message, shortened from
@ -133,7 +133,6 @@ pocket_read_more=مشہور مضامین:
# LOCALIZATION NOTE (pocket_read_even_more): This is shown as a link at the
# end of the list of popular topic links.
pocket_read_even_more=مزید کہانیاں دیکھیں
pocket_more_reccommendations=اور زیادہ سفارشات
pocket_learn_more=مزید سیکھیں
pocket_how_it_works=یہ کس طرح کام کرتا ہے
@ -158,6 +157,9 @@ manual_migration_import_button=ابھی درآمد کری
# LOCALIZATION NOTE (section_menu_action_*). These strings are displayed in the section
# context menu and are meant as a call to action for the given section.
section_menu_action_remove_section=صیغہ ہٹائیں
section_menu_action_collapse_section=صیغہ تفصیل سے دیکھیں
section_menu_action_expand_section=صیغہ کو توسیع کریں
section_menu_action_manage_section=صیغہ کابندرست کریں
section_menu_action_manage_webext=توسیع کابندرست کریں
section_menu_action_add_topsite=بہترین سائٹ شامل کریں
section_menu_action_add_search_engine=تلاش انجن کا اضافہ کریں
@ -174,7 +176,6 @@ section_menu_action_privacy_notice=رازداری کا نوٹس
firstrun_form_header=اپنی ای میل داخل کریں
firstrun_email_input_placeholder=ای میل
firstrun_invalid_input=جائز ای میل کی ظرورت ہے
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and

Просмотреть файл

@ -14,9 +14,10 @@ cd /activity-stream && npm install . && npm run buildmc
# Build latest m-c with Activity Stream changes
cd /mozilla-central && ./mach build \
&& ./mach lint -l codespell browser/components/newtab \
&& ./mach test browser/components/newtab/test/browser --headless \
&& ./mach test browser/components/newtab/test/xpcshell \
&& ./mach test --log-tbpl test_run_log \
browser_parsable_css \
browser/components/newtab \
browser/components/preferences/in-content/tests/browser_hometab_restore_defaults.js \
browser/components/preferences/in-content/tests/browser_newtab_menu.js \
browser/components/enterprisepolicies/tests/browser/browser_policy_set_homepage.js \

Просмотреть файл

@ -75,7 +75,7 @@ window.gActivityStreamStrings = {
"pocket_read_more": "Популярни теми:",
"pocket_read_even_more": "Повече публикации",
"pocket_more_reccommendations": "Повече препоръчани",
"pocket_how_it_works": "How it works",
"pocket_how_it_works": "Как работи",
"pocket_cta_button": "Вземете Pocket",
"pocket_cta_text": "Запазете статиите, които харесвате в Pocket и заредете ума си с увлекателни четива.",
"highlights_empty_state": "Разглеждайте и тук ще ви покажем някои от най-добрите статии, видео и други страници, които сте посетили или отметнали наскоро.",

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -107,6 +107,6 @@ window.gActivityStreamStrings = {
"firstrun_privacy_notice": "informativa sulla privacy",
"firstrun_continue_to_login": "Continua",
"firstrun_skip_login": "Ignora questo passaggio",
"context_menu_title": "Open menu",
"context_menu_title": "Apri menu",
"pocket_learn_more": "Ulteriori informazioni"
};

Просмотреть файл

@ -75,7 +75,7 @@ window.gActivityStreamStrings = {
"pocket_read_more": "Populārās tēmas:",
"pocket_read_even_more": "Parādīt vairāk lapas",
"pocket_more_reccommendations": "Vairāk ieteikumu",
"pocket_how_it_works": "How it works",
"pocket_how_it_works": "Kā tas strādā",
"pocket_cta_button": "Izmēģiniet Pocket",
"pocket_cta_text": "Saglabājiet interesantus stāstus Pocket un barojiet savu prātu ar interesantu lasāmvielu.",
"highlights_empty_state": "Sāciet pārlūkošanu un mēs šeit parādīsim lieliskus rakstus, video un citas apmeklētās lapas.",

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -107,6 +107,6 @@ window.gActivityStreamStrings = {
"firstrun_privacy_notice": "Privacyverklaring",
"firstrun_continue_to_login": "Doorgaan",
"firstrun_skip_login": "Deze stap overslaan",
"context_menu_title": "Open menu",
"context_menu_title": "Menu openen",
"pocket_learn_more": "Meer info"
};

Просмотреть файл

@ -75,7 +75,7 @@ window.gActivityStreamStrings = {
"pocket_read_more": "Temas populars:",
"pocket_read_even_more": "Mussar dapli artitgels",
"pocket_more_reccommendations": "Dapli propostas",
"pocket_how_it_works": "How it works",
"pocket_how_it_works": "Co ch'i funcziuna",
"pocket_cta_button": "Obtegnair Pocket",
"pocket_cta_text": "Memorisescha ils artitgels che ta plaschan en Pocket e procura per inspiraziun cuntinuanta cun lectura fascinanta.",
"highlights_empty_state": "Cumenza a navigar e nus ta mussain qua artitgels, videos ed autras paginas che ti has visità dacurt u che ti has agiuntà dacurt sco segnapagina.",

Просмотреть файл

@ -11,7 +11,7 @@ window.gActivityStreamStrings = {
"type_label_recommended": "رجحان سازی",
"type_label_pocket": "Pocket میں محفوظ شدہ",
"type_label_downloaded": "ڈاؤن لوڈ شدہ",
"menu_action_bookmark": "نشانی",
"menu_action_bookmark": "بک مارک",
"menu_action_remove_bookmark": "نشانى ہٹائيں",
"menu_action_open_new_window": "نئے دریچے میں کھولیں",
"menu_action_open_private_window": "نئی نجی دریچے میں کھولیں",
@ -54,7 +54,7 @@ window.gActivityStreamStrings = {
"settings_pane_button_label": "اپنے نئے ٹیب کہ صفحہ کی تخصیص کریں",
"settings_pane_topsites_header": "بہترین سائٹیں",
"settings_pane_highlights_header": "شہ سرخياں",
"settings_pane_highlights_options_bookmarks": "نشانیاں",
"settings_pane_highlights_options_bookmarks": "بک مارک",
"settings_pane_snippets_header": "سنپیٹ",
"edit_topsites_button_text": "تدوین",
"edit_topsites_edit_button": "اس سائٹ کی تدوین کریں",
@ -86,9 +86,9 @@ window.gActivityStreamStrings = {
"error_fallback_default_info": "Oops, something went wrong loading this content.",
"error_fallback_default_refresh_suggestion": "Refresh page to try again.",
"section_menu_action_remove_section": "صیغہ ہٹائیں",
"section_menu_action_collapse_section": "Collapse Section",
"section_menu_action_expand_section": "Expand Section",
"section_menu_action_manage_section": "Manage Section",
"section_menu_action_collapse_section": "صیغہ تفصیل سے دیکھیں",
"section_menu_action_expand_section": "صیغہ کو توسیع کریں",
"section_menu_action_manage_section": "صیغہ کابندرست کریں",
"section_menu_action_manage_webext": "توسیع کابندرست کریں",
"section_menu_action_add_topsite": "بہترین سائٹ شامل کریں",
"section_menu_action_add_search_engine": "تلاش انجن کا اضافہ کریں",

Просмотреть файл

@ -340,6 +340,36 @@ add_task(async function checkFrecentSites() {
await clearHistoryAndBookmarks();
});
add_task(async function check_pinned_sites() {
const originalPin = JSON.stringify(NewTabUtils.pinnedLinks.links);
const sitesToPin = [
{url: "https://foo.com"},
{url: "https://floogle.com", searchTopSite: true},
];
sitesToPin.forEach((site => NewTabUtils.pinnedLinks.pin(site, NewTabUtils.pinnedLinks.links.length)));
let message;
message = {id: "foo", targeting: "'https://foo.com' in pinnedSites|mapToProperty('url')"};
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select correct item by url in pinnedSites");
message = {id: "foo", targeting: "'foo.com' in pinnedSites|mapToProperty('host')"};
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select correct item by host in pinnedSites");
message = {id: "foo", targeting: "'floogle.com' in pinnedSites[.searchTopSite == true]|mapToProperty('host')"};
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select correct item by host and searchTopSite in pinnedSites");
// Cleanup
sitesToPin.forEach(site => NewTabUtils.pinnedLinks.unpin(site));
await clearHistoryAndBookmarks();
is(JSON.stringify(NewTabUtils.pinnedLinks.links), originalPin,
"should restore pinned sites to its original state");
});
add_task(async function check_firefox_version() {
const message = {id: "foo", targeting: "firefoxVersion > 0"};
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,

Просмотреть файл

@ -7,7 +7,7 @@ export const baseKeys = {
addon_version: Joi.string().required(),
locale: Joi.string().required(),
session_id: Joi.string(),
page: Joi.valid(["about:home", "about:newtab", "about:welcome", "unknown"]),
page: Joi.valid(["about:home", "about:newtab", "about:welcome", "both", "unknown"]),
user_prefs: Joi.number().integer().required(),
};
@ -24,12 +24,16 @@ export const eventsTelemetryExtraKeys = Joi.object().keys({
export const UserEventPing = Joi.object().keys(Object.assign({}, baseKeys, {
session_id: baseKeys.session_id.required(),
page: baseKeys.page.required(),
source: Joi.string().required(),
source: Joi.string(),
event: Joi.string().required(),
action: Joi.valid("activity_stream_user_event").required(),
metadata_source: Joi.string(),
highlight_type: Joi.valid(["bookmarks", "recommendation", "history"]),
recommender_type: Joi.string(),
value: Joi.object().keys({
newtab_url_category: Joi.string(),
home_url_category: Joi.string(),
}),
}));
export const UTUserEventPing = Joi.array().items(

Просмотреть файл

@ -208,7 +208,11 @@ describe("ASRouter", () => {
sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => true);
await Router.setState({foo: 123});
assert.calledWith(channel.sendAsyncMessage, "ASRouter:parent-to-child", {type: "ADMIN_SET_STATE", data: Router.state});
assert.calledOnce(channel.sendAsyncMessage);
assert.deepEqual(channel.sendAsyncMessage.firstCall.args[1], {
type: "ADMIN_SET_STATE",
data: Object.assign({}, Router.state, {providerPrefs: ASRouterPreferences.providers}),
});
});
it("should not send a message on a state change asrouter.devtoolsEnabled pref is on", async () => {
sandbox.stub(ASRouterPreferences, "devtoolsEnabled").get(() => false);
@ -393,452 +397,491 @@ describe("ASRouter", () => {
});
});
describe("#onMessage: SNIPPETS_REQUEST", () => {
it("should set state.lastMessageId to a message id", async () => {
await Router.onMessage(fakeAsyncMessage({type: "SNIPPETS_REQUEST"}));
describe("onMessage", () => {
describe("#onMessage: SNIPPETS_REQUEST", () => {
it("should set state.lastMessageId to a message id", async () => {
await Router.onMessage(fakeAsyncMessage({type: "SNIPPETS_REQUEST"}));
assert.include(ALL_MESSAGE_IDS, Router.state.lastMessageId);
});
it("should send a message back to the to the target", async () => {
// force the only message to be a regular message so getRandomItemFromArray picks it
await Router.setState({messages: [{id: "foo", template: "simple_template", content: {title: "Foo", body: "Foo123"}}]});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
const [currentMessage] = Router.state.messages.filter(message => message.id === Router.state.lastMessageId);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: currentMessage});
});
it("should send a message back to the to the target if there is a bundle, too", async () => {
// force the only message to be a bundled message so getRandomItemFromArray picks it
sandbox.stub(Router, "_findProvider").returns(null);
await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 1, content: {title: "Foo1", body: "Foo123-1"}}]});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
const [currentMessage] = Router.state.messages.filter(message => message.id === Router.state.lastMessageId);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME);
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].type, "SET_BUNDLED_MESSAGES");
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[0].content, currentMessage.content);
});
it("should properly order the message's bundle if specified", async () => {
// force the only messages to be a bundled messages so getRandomItemFromArray picks one of them
sandbox.stub(Router, "_findProvider").returns(null);
const firstMessage = {id: "foo2", template: "simple_template", bundled: 2, order: 1, content: {title: "Foo2", body: "Foo123-2"}};
const secondMessage = {id: "foo1", template: "simple_template", bundled: 2, order: 2, content: {title: "Foo1", body: "Foo123-1"}};
await Router.setState({messages: [secondMessage, firstMessage]});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME);
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].type, "SET_BUNDLED_MESSAGES");
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[0].content, firstMessage.content);
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[1].content, secondMessage.content);
});
it("should return a null bundle if we do not have enough messages to fill the bundle", async () => {
// force the only message to be a bundled message that needs 2 messages in the bundle
await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 2, content: {title: "Foo1", body: "Foo123-1"}}]});
const bundle = await Router._getBundledMessages(Router.state.messages[0]);
assert.equal(bundle, null);
});
it("should send down extra attributes in the bundle if they exist", async () => {
sandbox.stub(Router, "_findProvider").returns({getExtraAttributes() { return Promise.resolve({header: "header"}); }});
await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 1, content: {title: "Foo1", body: "Foo123-1"}}]});
const result = await Router._getBundledMessages(Router.state.messages[0]);
assert.equal(result.extraTemplateStrings.header, "header");
});
it("should send a CLEAR_ALL message if no bundle available", async () => {
// force the only message to be a bundled message that needs 2 messages in the bundle
await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 2, content: {title: "Foo1", body: "Foo123-1"}}]});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_ALL"});
});
it("should send a CLEAR_ALL message if no messages are available", async () => {
await Router.setState({messages: []});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
assert.include(ALL_MESSAGE_IDS, Router.state.lastMessageId);
});
it("should send a message back to the to the target", async () => {
// force the only message to be a regular message so getRandomItemFromArray picks it
await Router.setState({messages: [{id: "foo", template: "simple_template", content: {title: "Foo", body: "Foo123"}}]});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
const [currentMessage] = Router.state.messages.filter(message => message.id === Router.state.lastMessageId);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: currentMessage});
});
it("should send a message back to the to the target if there is a bundle, too", async () => {
// force the only message to be a bundled message so getRandomItemFromArray picks it
sandbox.stub(Router, "_findProvider").returns(null);
await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 1, content: {title: "Foo1", body: "Foo123-1"}}]});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
const [currentMessage] = Router.state.messages.filter(message => message.id === Router.state.lastMessageId);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME);
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].type, "SET_BUNDLED_MESSAGES");
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[0].content, currentMessage.content);
});
it("should properly order the message's bundle if specified", async () => {
// force the only messages to be a bundled messages so getRandomItemFromArray picks one of them
sandbox.stub(Router, "_findProvider").returns(null);
const firstMessage = {id: "foo2", template: "simple_template", bundled: 2, order: 1, content: {title: "Foo2", body: "Foo123-2"}};
const secondMessage = {id: "foo1", template: "simple_template", bundled: 2, order: 2, content: {title: "Foo1", body: "Foo123-1"}};
await Router.setState({messages: [secondMessage, firstMessage]});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME);
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].type, "SET_BUNDLED_MESSAGES");
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[0].content, firstMessage.content);
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[1].content, secondMessage.content);
});
it("should return a null bundle if we do not have enough messages to fill the bundle", async () => {
// force the only message to be a bundled message that needs 2 messages in the bundle
await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 2, content: {title: "Foo1", body: "Foo123-1"}}]});
const bundle = await Router._getBundledMessages(Router.state.messages[0]);
assert.equal(bundle, null);
});
it("should send down extra attributes in the bundle if they exist", async () => {
sandbox.stub(Router, "_findProvider").returns({getExtraAttributes() { return Promise.resolve({header: "header"}); }});
await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 1, content: {title: "Foo1", body: "Foo123-1"}}]});
const result = await Router._getBundledMessages(Router.state.messages[0]);
assert.equal(result.extraTemplateStrings.header, "header");
});
it("should send a CLEAR_ALL message if no bundle available", async () => {
// force the only message to be a bundled message that needs 2 messages in the bundle
await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 2, content: {title: "Foo1", body: "Foo123-1"}}]});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_ALL"});
});
it("should send a CLEAR_ALL message if no messages are available", async () => {
await Router.setState({messages: []});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_ALL"});
});
it("should make a request to the provided endpoint on SNIPPETS_REQUEST", async () => {
const url = "https://snippets-admin.mozilla.org/foo";
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_ALL"});
});
it("should make a request to the provided endpoint on SNIPPETS_REQUEST", async () => {
const url = "https://snippets-admin.mozilla.org/foo";
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.calledWith(global.fetch, url);
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
});
it("should make a request to the provided endpoint on ADMIN_CONNECT_STATE and remove the endpoint", async () => {
const url = "https://snippets-admin.mozilla.org/foo";
const msg = fakeAsyncMessage({type: "ADMIN_CONNECT_STATE", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.calledWith(global.fetch, url);
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
});
it("should make a request to the provided endpoint on ADMIN_CONNECT_STATE and remove the endpoint", async () => {
const url = "https://snippets-admin.mozilla.org/foo";
const msg = fakeAsyncMessage({type: "ADMIN_CONNECT_STATE", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.calledWith(global.fetch, url);
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
});
it("should dispatch SNIPPETS_PREVIEW_MODE when adding a preview endpoint", async () => {
const url = "https://snippets-admin.mozilla.org/foo";
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.calledWith(global.fetch, url);
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
});
it("should dispatch SNIPPETS_PREVIEW_MODE when adding a preview endpoint", async () => {
const url = "https://snippets-admin.mozilla.org/foo";
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.calledWithExactly(Router.dispatchToAS, ac.OnlyToOneContent({type: "SNIPPETS_PREVIEW_MODE"}, msg.target.portID));
});
it("should not add a url that is not from a whitelisted host", async () => {
const url = "https://mozilla.org";
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.calledWithExactly(Router.dispatchToAS, ac.OnlyToOneContent({type: "SNIPPETS_PREVIEW_MODE"}, msg.target.portID));
});
it("should not add a url that is not from a whitelisted host", async () => {
const url = "https://mozilla.org";
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
});
it("should reject bad urls", async () => {
const url = "foo";
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
});
it("should reject bad urls", async () => {
const url = "foo";
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
});
});
describe("#onMessage: BLOCK_MESSAGE_BY_ID", () => {
it("should add the id to the messageBlockList and broadcast a CLEAR_MESSAGE message with the id", async () => {
await Router.setState({lastMessageId: "foo"});
const msg = fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo"}});
await Router.onMessage(msg);
assert.isTrue(Router.state.messageBlockList.includes("foo"));
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_MESSAGE", data: {id: "foo"}});
});
it("should not broadcast CLEAR_MESSAGE if preventDismiss is true", async () => {
const msg = fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo", preventDismiss: true}});
await Router.onMessage(msg);
assert.notCalled(channel.sendAsyncMessage);
});
});
describe("#onMessage: DISMISS_MESSAGE_BY_ID", () => {
it("should reply with CLEAR_MESSAGE with the correct id", async () => {
const msg = fakeAsyncMessage({type: "DISMISS_MESSAGE_BY_ID", data: {id: "foo"}});
await Router.onMessage(msg);
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_MESSAGE", data: {id: "foo"}});
});
});
describe("#onMessage: BLOCK_PROVIDER_BY_ID", () => {
it("should add the provider id to the providerBlockList and broadcast a CLEAR_PROVIDER with the provider id", async () => {
const msg = fakeAsyncMessage({type: "BLOCK_PROVIDER_BY_ID", data: {id: "bar"}});
await Router.onMessage(msg);
assert.isTrue(Router.state.providerBlockList.includes("bar"));
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_PROVIDER", data: {id: "bar"}});
});
});
describe("#onMessage: BLOCK_BUNDLE", () => {
it("should add all the ids in the bundle to the messageBlockList and send a CLEAR_BUNDLE message", async () => {
const bundleIds = [FAKE_BUNDLE[0].id, FAKE_BUNDLE[1].id];
await Router.setState({lastMessageId: "foo"});
const msg = fakeAsyncMessage({type: "BLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}});
await Router.onMessage(msg);
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_BUNDLE"});
assert.calledWithExactly(Router._storage.set, "messageBlockList", bundleIds);
});
});
describe("#onMessage: UNBLOCK_MESSAGE_BY_ID", () => {
it("should remove the id from the messageBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
assert.isTrue(Router.state.messageBlockList.includes("foo"));
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
assert.isFalse(Router.state.messageBlockList.includes("foo"));
});
it("should save the messageBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
});
});
describe("#onMessage: UNBLOCK_PROVIDER_BY_ID", () => {
it("should remove the id from the providerBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
assert.isTrue(Router.state.providerBlockList.includes("foo"));
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
assert.isFalse(Router.state.providerBlockList.includes("foo"));
});
it("should save the providerBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
assert.calledWithExactly(Router._storage.set, "providerBlockList", []);
});
});
describe("#onMessage: UNBLOCK_BUNDLE", () => {
it("should remove all the ids in the bundle from the messageBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
assert.isFalse(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
assert.isFalse(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
});
it("should save the messageBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
});
});
describe("#onMessage: ADMIN_CONNECT_STATE", () => {
it("should send a message containing the whole state", async () => {
const msg = fakeAsyncMessage({type: "ADMIN_CONNECT_STATE"});
await Router.onMessage(msg);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "ADMIN_SET_STATE", data: Router.state});
});
});
describe("#onMessage: SNIPPETS_REQUEST", () => {
it("should call sendNextMessage on SNIPPETS_REQUEST", async () => {
sandbox.stub(Router, "sendNextMessage").resolves();
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
assert.calledOnce(Router.sendNextMessage);
assert.calledWithExactly(Router.sendNextMessage, sinon.match.instanceOf(FakeRemotePageManager), {});
});
it("should return the preview message if that's available and remove it from Router.state", async () => {
const expectedObj = {provider: "preview"};
Router.setState({messages: [expectedObj]});
await Router.sendNextMessage(channel);
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: expectedObj});
assert.isUndefined(Router.state.messages.find(m => m.provider === "preview"));
});
it("should call _getBundledMessages if we request a message that needs to be bundled", async () => {
sandbox.stub(Router, "_getBundledMessages").resolves();
// forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)
const [, testMessage] = Router.state.messages;
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
await Router.onMessage(msg);
assert.calledOnce(Router._getBundledMessages);
});
it("should properly pick another message of the same template if it is bundled; force = true", async () => {
// forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)
const [, testMessage1, testMessage2] = Router.state.messages;
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage1.id}});
await Router.onMessage(msg);
// Expected object should have some properties of the original message it picked (testMessage1)
// plus the bundled content of the others that it picked of the same template (testMessage2)
const expectedObj = {
template: testMessage1.template,
provider: testMessage1.provider,
bundle: [{content: testMessage1.content, id: testMessage1.id, order: 1}, {content: testMessage2.content, id: testMessage2.id}],
};
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: expectedObj});
});
it("should properly pick another message of the same template if it is bundled; force = false", async () => {
// forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)
const [, testMessage1, testMessage2] = Router.state.messages;
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage1.id}});
await Router.setMessageById(testMessage1.id, msg.target, false);
// Expected object should have some properties of the original message it picked (testMessage1)
// plus the bundled content of the others that it picked of the same template (testMessage2)
const expectedObj = {
template: testMessage1.template,
provider: testMessage1.provider,
bundle: [{content: testMessage1.content, id: testMessage1.id, order: 1}, {content: testMessage2.content, id: testMessage2.id, order: 2}],
};
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: expectedObj});
});
it("should get the bundle and send the message if the message has a bundle", async () => {
sandbox.stub(Router, "sendNextMessage").resolves();
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
msg.bundled = 2; // force this message to want to be bundled
await Router.onMessage(msg);
assert.calledOnce(Router.sendNextMessage);
});
});
describe("#onMessage: TRIGGER", () => {
it("should pass the trigger to ASRouterTargeting on TRIGGER message", async () => {
sandbox.stub(Router, "_findMessage").resolves();
const msg = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "firstRun"}}});
await Router.onMessage(msg);
assert.calledOnce(Router._findMessage);
assert.deepEqual(Router._findMessage.firstCall.args[1], {id: "firstRun"});
});
it("consider the trigger when picking a message", async () => {
const messages = [
{id: "foo1", template: "simple_template", bundled: 1, trigger: {id: "foo"}, content: {title: "Foo1", body: "Foo123-1"}},
];
const {data} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
const message = await Router._findMessage(messages, data.data.trigger);
assert.equal(message, messages[0]);
});
it("should pick a message with the right targeting and trigger", async () => {
let messages = [
{id: "foo1", template: "simple_template", bundled: 2, trigger: {id: "foo"}, content: {title: "Foo1", body: "Foo123-1"}},
{id: "foo2", template: "simple_template", bundled: 2, trigger: {id: "bar"}, content: {title: "Foo2", body: "Foo123-2"}},
{id: "foo3", template: "simple_template", bundled: 2, trigger: {id: "foo"}, content: {title: "Foo3", body: "Foo123-3"}},
];
sandbox.stub(Router, "_findProvider").returns(null);
await Router.setState({messages});
const {target} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
let {bundle} = await Router._getBundledMessages(messages[0], target, {id: "foo"});
assert.equal(bundle.length, 2);
// it should have picked foo1 and foo3 only
assert.isTrue(bundle.every(elem => elem.id === "foo1" || elem.id === "foo3"));
});
it("should have previousSessionEnd in the message context", () => {
assert.propertyVal(Router._getMessagesContext(), "previousSessionEnd", 100);
});
});
describe("#onMessage: OVERRIDE_MESSAGE", () => {
it("should broadcast a SET_MESSAGE message to all clients with a particular id", async () => {
const [testMessage] = Router.state.messages;
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
await Router.onMessage(msg);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: testMessage});
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
});
});
it("should call CFRPageActions.forceRecommendation if the template is cfr_action and force is true", async () => {
sandbox.stub(CFRPageActions, "forceRecommendation");
const testMessage = {id: "foo", template: "cfr_doorhanger"};
await Router.setState({messages: [testMessage]});
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
await Router.onMessage(msg);
describe("#onMessage: BLOCK_MESSAGE_BY_ID", () => {
it("should add the id to the messageBlockList and broadcast a CLEAR_MESSAGE message with the id", async () => {
await Router.setState({lastMessageId: "foo"});
const msg = fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo"}});
await Router.onMessage(msg);
assert.notCalled(msg.target.sendAsyncMessage);
assert.calledOnce(CFRPageActions.forceRecommendation);
assert.isTrue(Router.state.messageBlockList.includes("foo"));
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_MESSAGE", data: {id: "foo"}});
});
it("should not broadcast CLEAR_MESSAGE if preventDismiss is true", async () => {
const msg = fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo", preventDismiss: true}});
await Router.onMessage(msg);
assert.notCalled(channel.sendAsyncMessage);
});
});
it("should call CFRPageActions.addRecommendation if the template is cfr_action and force is false", async () => {
sandbox.stub(CFRPageActions, "addRecommendation");
const testMessage = {id: "foo", template: "cfr_doorhanger"};
await Router.setState({messages: [testMessage]});
await Router._sendMessageToTarget(testMessage, {}, {}, false);
describe("#onMessage: DISMISS_MESSAGE_BY_ID", () => {
it("should reply with CLEAR_MESSAGE with the correct id", async () => {
const msg = fakeAsyncMessage({type: "DISMISS_MESSAGE_BY_ID", data: {id: "foo"}});
assert.calledOnce(CFRPageActions.addRecommendation);
await Router.onMessage(msg);
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_MESSAGE", data: {id: "foo"}});
});
});
it("should broadcast CLEAR_ALL if provided id did not resolve to a message", async () => {
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: -1}});
await Router.onMessage(msg);
describe("#onMessage: BLOCK_PROVIDER_BY_ID", () => {
it("should add the provider id to the providerBlockList and broadcast a CLEAR_PROVIDER with the provider id", async () => {
const msg = fakeAsyncMessage({type: "BLOCK_PROVIDER_BY_ID", data: {id: "bar"}});
await Router.onMessage(msg);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_ALL"});
assert.isTrue(Router.state.providerBlockList.includes("bar"));
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_PROVIDER", data: {id: "bar"}});
});
});
});
describe("#onMessage: Onboarding actions", () => {
it("should call OpenBrowserWindow with a private window on OPEN_PRIVATE_BROWSER_WINDOW", async () => {
let [testMessage] = Router.state.messages;
const msg = fakeExecuteUserAction({type: "OPEN_PRIVATE_BROWSER_WINDOW", data: testMessage});
await Router.onMessage(msg);
describe("#onMessage: BLOCK_BUNDLE", () => {
it("should add all the ids in the bundle to the messageBlockList and send a CLEAR_BUNDLE message", async () => {
const bundleIds = [FAKE_BUNDLE[0].id, FAKE_BUNDLE[1].id];
await Router.setState({lastMessageId: "foo"});
const msg = fakeAsyncMessage({type: "BLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}});
await Router.onMessage(msg);
assert.calledWith(msg.target.browser.ownerGlobal.OpenBrowserWindow, {private: true});
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_BUNDLE"});
assert.calledWithExactly(Router._storage.set, "messageBlockList", bundleIds);
});
});
it("should call openLinkIn with the correct params on OPEN_URL", async () => {
let [testMessage] = Router.state.messages;
testMessage.button_action = {type: "OPEN_URL", data: {args: "some/url.com"}};
const msg = fakeExecuteUserAction(testMessage.button_action);
await Router.onMessage(msg);
assert.calledOnce(msg.target.browser.ownerGlobal.openLinkIn);
assert.calledWith(msg.target.browser.ownerGlobal.openLinkIn,
"some/url.com", "tabshifted", {"private": false, "triggeringPrincipal": undefined});
describe("#onMessage: UNBLOCK_MESSAGE_BY_ID", () => {
it("should remove the id from the messageBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
assert.isTrue(Router.state.messageBlockList.includes("foo"));
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
assert.isFalse(Router.state.messageBlockList.includes("foo"));
});
it("should save the messageBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
});
});
it("should call openLinkIn with the correct params on OPEN_ABOUT_PAGE", async () => {
let [testMessage] = Router.state.messages;
testMessage.button_action = {type: "OPEN_ABOUT_PAGE", data: {args: "something"}};
const msg = fakeExecuteUserAction(testMessage.button_action);
await Router.onMessage(msg);
assert.calledOnce(msg.target.browser.ownerGlobal.openTrustedLinkIn);
assert.calledWith(msg.target.browser.ownerGlobal.openTrustedLinkIn, "about:something", "tab");
describe("#onMessage: UNBLOCK_PROVIDER_BY_ID", () => {
it("should remove the id from the providerBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
assert.isTrue(Router.state.providerBlockList.includes("foo"));
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
assert.isFalse(Router.state.providerBlockList.includes("foo"));
});
it("should save the providerBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
assert.calledWithExactly(Router._storage.set, "providerBlockList", []);
});
});
});
describe("#onMessage: SHOW_FIREFOX_ACCOUNTS", () => {
let globals;
beforeEach(() => {
globals = new GlobalOverrider();
globals.set("FxAccounts", {config: {promiseSignUpURI: sandbox.stub().resolves("some/url")}});
describe("#onMessage: UNBLOCK_BUNDLE", () => {
it("should remove all the ids in the bundle from the messageBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
assert.isFalse(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
assert.isFalse(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
});
it("should save the messageBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
});
});
it("should call openLinkIn with the correct params on OPEN_URL", async () => {
let [testMessage] = Router.state.messages;
testMessage.button_action = {type: "SHOW_FIREFOX_ACCOUNTS"};
const msg = fakeExecuteUserAction(testMessage.button_action);
await Router.onMessage(msg);
assert.calledOnce(msg.target.browser.ownerGlobal.openLinkIn);
assert.calledWith(msg.target.browser.ownerGlobal.openLinkIn,
"some/url", "tabshifted", {"private": false, "triggeringPrincipal": undefined});
describe("#onMessage: ADMIN_CONNECT_STATE", () => {
it("should send a message containing the whole state", async () => {
const msg = fakeAsyncMessage({type: "ADMIN_CONNECT_STATE"});
await Router.onMessage(msg);
assert.calledOnce(msg.target.sendAsyncMessage);
assert.deepEqual(msg.target.sendAsyncMessage.firstCall.args[1], {
type: "ADMIN_SET_STATE",
data: Object.assign({}, Router.state, {providerPrefs: ASRouterPreferences.providers}),
});
});
});
});
describe("#onMessage: INSTALL_ADDON_FROM_URL", () => {
it("should call installAddonFromURL with correct arguments", async () => {
sandbox.stub(MessageLoaderUtils, "installAddonFromURL").resolves(null);
const msg = fakeExecuteUserAction({type: "INSTALL_ADDON_FROM_URL", data: {url: "foo.com"}});
describe("#onMessage: SNIPPETS_REQUEST", () => {
it("should call sendNextMessage on SNIPPETS_REQUEST", async () => {
sandbox.stub(Router, "sendNextMessage").resolves();
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
await Router.onMessage(msg);
assert.calledOnce(MessageLoaderUtils.installAddonFromURL);
assert.calledWithExactly(MessageLoaderUtils.installAddonFromURL, msg.target.browser, "foo.com");
assert.calledOnce(Router.sendNextMessage);
assert.calledWithExactly(Router.sendNextMessage, sinon.match.instanceOf(FakeRemotePageManager), {});
});
it("should return the preview message if that's available and remove it from Router.state", async () => {
const expectedObj = {provider: "preview"};
Router.setState({messages: [expectedObj]});
await Router.sendNextMessage(channel);
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: expectedObj});
assert.isUndefined(Router.state.messages.find(m => m.provider === "preview"));
});
it("should call _getBundledMessages if we request a message that needs to be bundled", async () => {
sandbox.stub(Router, "_getBundledMessages").resolves();
// forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)
const [, testMessage] = Router.state.messages;
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
await Router.onMessage(msg);
assert.calledOnce(Router._getBundledMessages);
});
it("should properly pick another message of the same template if it is bundled; force = true", async () => {
// forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)
const [, testMessage1, testMessage2] = Router.state.messages;
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage1.id}});
await Router.onMessage(msg);
// Expected object should have some properties of the original message it picked (testMessage1)
// plus the bundled content of the others that it picked of the same template (testMessage2)
const expectedObj = {
template: testMessage1.template,
provider: testMessage1.provider,
bundle: [{content: testMessage1.content, id: testMessage1.id, order: 1}, {content: testMessage2.content, id: testMessage2.id}],
};
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: expectedObj});
});
it("should properly pick another message of the same template if it is bundled; force = false", async () => {
// forcefully pick a message which needs to be bundled (the second message in FAKE_LOCAL_MESSAGES)
const [, testMessage1, testMessage2] = Router.state.messages;
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage1.id}});
await Router.setMessageById(testMessage1.id, msg.target, false);
// Expected object should have some properties of the original message it picked (testMessage1)
// plus the bundled content of the others that it picked of the same template (testMessage2)
const expectedObj = {
template: testMessage1.template,
provider: testMessage1.provider,
bundle: [{content: testMessage1.content, id: testMessage1.id, order: 1}, {content: testMessage2.content, id: testMessage2.id, order: 2}],
};
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: expectedObj});
});
it("should get the bundle and send the message if the message has a bundle", async () => {
sandbox.stub(Router, "sendNextMessage").resolves();
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
msg.bundled = 2; // force this message to want to be bundled
await Router.onMessage(msg);
assert.calledOnce(Router.sendNextMessage);
});
});
});
describe("#dispatch(action, target)", () => {
it("should an action and target to onMessage", async () => {
// use the IMPRESSION action to make sure actions are actually getting processed
sandbox.stub(Router, "addImpression");
sandbox.spy(Router, "onMessage");
const target = {};
const action = {type: "IMPRESSION"};
describe("#onMessage: TRIGGER", () => {
it("should pass the trigger to ASRouterTargeting on TRIGGER message", async () => {
sandbox.stub(Router, "_findMessage").resolves();
const msg = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "firstRun"}}});
await Router.onMessage(msg);
Router.dispatch(action, target);
assert.calledOnce(Router._findMessage);
assert.deepEqual(Router._findMessage.firstCall.args[1], {id: "firstRun"});
});
it("consider the trigger when picking a message", async () => {
const messages = [
{id: "foo1", template: "simple_template", bundled: 1, trigger: {id: "foo"}, content: {title: "Foo1", body: "Foo123-1"}},
];
assert.calledWith(Router.onMessage, {data: action, target});
assert.calledOnce(Router.addImpression);
const {data} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
const message = await Router._findMessage(messages, data.data.trigger);
assert.equal(message, messages[0]);
});
it("should pick a message with the right targeting and trigger", async () => {
let messages = [
{id: "foo1", template: "simple_template", bundled: 2, trigger: {id: "foo"}, content: {title: "Foo1", body: "Foo123-1"}},
{id: "foo2", template: "simple_template", bundled: 2, trigger: {id: "bar"}, content: {title: "Foo2", body: "Foo123-2"}},
{id: "foo3", template: "simple_template", bundled: 2, trigger: {id: "foo"}, content: {title: "Foo3", body: "Foo123-3"}},
];
sandbox.stub(Router, "_findProvider").returns(null);
await Router.setState({messages});
const {target} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
let {bundle} = await Router._getBundledMessages(messages[0], target, {id: "foo"});
assert.equal(bundle.length, 2);
// it should have picked foo1 and foo3 only
assert.isTrue(bundle.every(elem => elem.id === "foo1" || elem.id === "foo3"));
});
it("should have previousSessionEnd in the message context", () => {
assert.propertyVal(Router._getMessagesContext(), "previousSessionEnd", 100);
});
});
});
describe("#onMessage: DOORHANGER_TELEMETRY", () => {
it("should dispatch an AS_ROUTER_TELEMETRY_USER_EVENT on DOORHANGER_TELEMETRY message", async () => {
const msg = fakeAsyncMessage({type: "DOORHANGER_TELEMETRY", data: {message_id: "foo"}});
dispatchStub.reset();
describe("#onMessage: OVERRIDE_MESSAGE", () => {
it("should broadcast a SET_MESSAGE message to all clients with a particular id", async () => {
const [testMessage] = Router.state.messages;
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
await Router.onMessage(msg);
await Router.onMessage(msg);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: testMessage});
});
assert.calledOnce(dispatchStub);
const [action] = dispatchStub.firstCall.args;
assert.equal(action.type, "AS_ROUTER_TELEMETRY_USER_EVENT");
assert.equal(action.data.message_id, "foo");
it("should call CFRPageActions.forceRecommendation if the template is cfr_action and force is true", async () => {
sandbox.stub(CFRPageActions, "forceRecommendation");
const testMessage = {id: "foo", template: "cfr_doorhanger"};
await Router.setState({messages: [testMessage]});
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
await Router.onMessage(msg);
assert.notCalled(msg.target.sendAsyncMessage);
assert.calledOnce(CFRPageActions.forceRecommendation);
});
it("should call CFRPageActions.addRecommendation if the template is cfr_action and force is false", async () => {
sandbox.stub(CFRPageActions, "addRecommendation");
const testMessage = {id: "foo", template: "cfr_doorhanger"};
await Router.setState({messages: [testMessage]});
await Router._sendMessageToTarget(testMessage, {}, {}, false);
assert.calledOnce(CFRPageActions.addRecommendation);
});
it("should broadcast CLEAR_ALL if provided id did not resolve to a message", async () => {
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: -1}});
await Router.onMessage(msg);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_ALL"});
});
});
});
describe("#onMessage: EXPIRE_QUERY_CACHE", () => {
it("should clear all QueryCache getters", async () => {
const msg = fakeAsyncMessage({type: "EXPIRE_QUERY_CACHE"});
sandbox.stub(QueryCache, "expireAll");
describe("#onMessage: Onboarding actions", () => {
it("should call OpenBrowserWindow with a private window on OPEN_PRIVATE_BROWSER_WINDOW", async () => {
let [testMessage] = Router.state.messages;
const msg = fakeExecuteUserAction({type: "OPEN_PRIVATE_BROWSER_WINDOW", data: testMessage});
await Router.onMessage(msg);
await Router.onMessage(msg);
assert.calledWith(msg.target.browser.ownerGlobal.OpenBrowserWindow, {private: true});
});
it("should call openLinkIn with the correct params on OPEN_URL", async () => {
let [testMessage] = Router.state.messages;
testMessage.button_action = {type: "OPEN_URL", data: {args: "some/url.com"}};
const msg = fakeExecuteUserAction(testMessage.button_action);
await Router.onMessage(msg);
assert.calledOnce(QueryCache.expireAll);
assert.calledOnce(msg.target.browser.ownerGlobal.openLinkIn);
assert.calledWith(msg.target.browser.ownerGlobal.openLinkIn,
"some/url.com", "tabshifted", {"private": false, "triggeringPrincipal": undefined});
});
it("should call openLinkIn with the correct params on OPEN_ABOUT_PAGE", async () => {
let [testMessage] = Router.state.messages;
testMessage.button_action = {type: "OPEN_ABOUT_PAGE", data: {args: "something"}};
const msg = fakeExecuteUserAction(testMessage.button_action);
await Router.onMessage(msg);
assert.calledOnce(msg.target.browser.ownerGlobal.openTrustedLinkIn);
assert.calledWith(msg.target.browser.ownerGlobal.openTrustedLinkIn, "about:something", "tab");
});
});
describe("#onMessage: SHOW_FIREFOX_ACCOUNTS", () => {
let globals;
beforeEach(() => {
globals = new GlobalOverrider();
globals.set("FxAccounts", {config: {promiseSignUpURI: sandbox.stub().resolves("some/url")}});
});
it("should call openLinkIn with the correct params on OPEN_URL", async () => {
let [testMessage] = Router.state.messages;
testMessage.button_action = {type: "SHOW_FIREFOX_ACCOUNTS"};
const msg = fakeExecuteUserAction(testMessage.button_action);
await Router.onMessage(msg);
assert.calledOnce(msg.target.browser.ownerGlobal.openLinkIn);
assert.calledWith(msg.target.browser.ownerGlobal.openLinkIn,
"some/url", "tabshifted", {"private": false, "triggeringPrincipal": undefined});
});
});
describe("#onMessage: INSTALL_ADDON_FROM_URL", () => {
it("should call installAddonFromURL with correct arguments", async () => {
sandbox.stub(MessageLoaderUtils, "installAddonFromURL").resolves(null);
const msg = fakeExecuteUserAction({type: "INSTALL_ADDON_FROM_URL", data: {url: "foo.com"}});
await Router.onMessage(msg);
assert.calledOnce(MessageLoaderUtils.installAddonFromURL);
assert.calledWithExactly(MessageLoaderUtils.installAddonFromURL, msg.target.browser, "foo.com");
});
});
describe("#dispatch(action, target)", () => {
it("should an action and target to onMessage", async () => {
// use the IMPRESSION action to make sure actions are actually getting processed
sandbox.stub(Router, "addImpression");
sandbox.spy(Router, "onMessage");
const target = {};
const action = {type: "IMPRESSION"};
Router.dispatch(action, target);
assert.calledWith(Router.onMessage, {data: action, target});
assert.calledOnce(Router.addImpression);
});
});
describe("#onMessage: DOORHANGER_TELEMETRY", () => {
it("should dispatch an AS_ROUTER_TELEMETRY_USER_EVENT on DOORHANGER_TELEMETRY message", async () => {
const msg = fakeAsyncMessage({type: "DOORHANGER_TELEMETRY", data: {message_id: "foo"}});
dispatchStub.reset();
await Router.onMessage(msg);
assert.calledOnce(dispatchStub);
const [action] = dispatchStub.firstCall.args;
assert.equal(action.type, "AS_ROUTER_TELEMETRY_USER_EVENT");
assert.equal(action.data.message_id, "foo");
});
});
describe("#onMessage: EXPIRE_QUERY_CACHE", () => {
it("should clear all QueryCache getters", async () => {
const msg = fakeAsyncMessage({type: "EXPIRE_QUERY_CACHE"});
sandbox.stub(QueryCache, "expireAll");
await Router.onMessage(msg);
assert.calledOnce(QueryCache.expireAll);
});
});
describe("#onMessage: ENABLE_PROVIDER", () => {
it("should enable the provider via ASRouterPreferences", async () => {
const msg = fakeAsyncMessage({type: "ENABLE_PROVIDER", data: "foo"});
sandbox.stub(ASRouterPreferences, "enableOrDisableProvider");
await Router.onMessage(msg);
assert.calledWith(ASRouterPreferences.enableOrDisableProvider, "foo", true);
});
});
describe("#onMessage: DISABLE_PROVIDER", () => {
it("should disable the provider via ASRouterPreferences", async () => {
const msg = fakeAsyncMessage({type: "DISABLE_PROVIDER", data: "foo"});
sandbox.stub(ASRouterPreferences, "enableOrDisableProvider");
await Router.onMessage(msg);
assert.calledWith(ASRouterPreferences.enableOrDisableProvider, "foo", false);
});
});
describe("#onMessage: RESET_PROVIDER_PREF", () => {
it("should reset provider pref via ASRouterPreferences", async () => {
const msg = fakeAsyncMessage({type: "RESET_PROVIDER_PREF", data: "foo"});
sandbox.stub(ASRouterPreferences, "resetProviderPref");
await Router.onMessage(msg);
assert.calledOnce(ASRouterPreferences.resetProviderPref);
});
});
});

Просмотреть файл

@ -9,8 +9,9 @@ const SNIPPETS_USER_PREF = "browser.newtabpage.activity-stream.feeds.snippets";
* 1. asrouter.messageProvider
* 2. asrouter.devtoolsEnabled
* 3. browser.newtabpage.activity-stream.feeds.snippets (user preference - snippets)
* 4. browser.newtabpage.activity-stream.asrouter.userprefs.cfr (user preference - cfr)
*/
const NUMBER_OF_PREFS_TO_OBSERVE = 3;
const NUMBER_OF_PREFS_TO_OBSERVE = 4;
describe("ASRouterPreferences", () => {
let ASRouterPreferences;
@ -163,6 +164,45 @@ describe("ASRouterPreferences", () => {
assert.isTrue(ASRouterPreferences.getUserPreference("snippets"));
});
});
describe("#enableOrDisableProvider", () => {
it("should enable an existing provider if second param is true", () => {
const setStub = sandbox.stub(global.Services.prefs, "setStringPref");
stringPrefStub.withArgs(PROVIDER_PREF).returns(JSON.stringify([{id: "foo", enabled: false}, {id: "bar", enabled: false}]));
assert.isFalse(ASRouterPreferences.providers[0].enabled);
ASRouterPreferences.enableOrDisableProvider("foo", true);
assert.calledWith(setStub, PROVIDER_PREF, JSON.stringify([{id: "foo", enabled: true}, {id: "bar", enabled: false}]));
});
it("should disable an existing provider if second param is false", () => {
const setStub = sandbox.stub(global.Services.prefs, "setStringPref");
stringPrefStub.withArgs(PROVIDER_PREF).returns(JSON.stringify([{id: "foo", enabled: true}, {id: "bar", enabled: true}]));
assert.isTrue(ASRouterPreferences.providers[0].enabled);
ASRouterPreferences.enableOrDisableProvider("foo", false);
assert.calledWith(setStub, PROVIDER_PREF, JSON.stringify([{id: "foo", enabled: false}, {id: "bar", enabled: true}]));
});
it("should not throw if the id does not exist", () => {
assert.doesNotThrow(() => {
ASRouterPreferences.enableOrDisableProvider("does_not_exist", true);
});
});
it("should not throw if pref is not parseable", () => {
stringPrefStub.withArgs(PROVIDER_PREF).returns("not valid");
assert.doesNotThrow(() => {
ASRouterPreferences.enableOrDisableProvider("foo", true);
});
});
});
describe("#resetProviderPref", () => {
it("should reset the pref and user prefs", () => {
const resetStub = sandbox.stub(global.Services.prefs, "clearUserPref");
ASRouterPreferences.resetProviderPref();
assert.calledWith(resetStub, PROVIDER_PREF);
assert.calledWith(resetStub, SNIPPETS_USER_PREF);
});
});
describe("observer, listeners", () => {
it("should invalidate .providers when the pref is changed", () => {
const testProviders = [{id: "newstuff"}];

Просмотреть файл

@ -1,4 +1,7 @@
import {convertLinks} from "content-src/asrouter/components/RichText/RichText";
import {convertLinks, RichText} from "content-src/asrouter/components/RichText/RichText";
import {Localized} from "fluent-react";
import {mount} from "enzyme";
import React from "react";
describe("convertLinks", () => {
let sandbox;
@ -40,4 +43,10 @@ describe("convertLinks", () => {
assert.propertyVal(result.cta.props, "data-args", cta.args);
assert.propertyVal(result.cta.props, "onClick", stub);
});
it("should allow for custom elements & styles", () => {
const wrapper = mount(<RichText customElements={{em: <em style={{color: "#f05"}} />}} text="<em>foo</em>" localization_id="text" />);
const localized = wrapper.find(Localized);
assert.propertyVal(localized.props().em.props.style, "color", "#f05");
});
});

Просмотреть файл

@ -1,3 +1,4 @@
import EOYSnippetSchema from "../../../content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json";
import SimpleSnippetSchema from "../../../content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json";
import {SnippetsTestMessageProvider} from "../../../lib/SnippetsTestMessageProvider.jsm";
import SubmitFormSnippetSchema from "../../../content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json";
@ -7,6 +8,7 @@ const schemas = {
"newsletter_snippet": SubmitFormSnippetSchema,
"fxa_signup_snippet": SubmitFormSnippetSchema,
"send_to_device_snippet": SubmitFormSnippetSchema,
"eoy_snippet": EOYSnippetSchema,
};
describe("SnippetsTestMessageProvider", () => {

Просмотреть файл

@ -0,0 +1,26 @@
import EOYSnippetSchema from "content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json";
import {expectedValues} from "./snippets-fx57";
import SimpleSnippetSchema from "content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json";
import SubmitFormSchema from "content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json";
export const SnippetsSchemas = {
eoy_snippet: EOYSnippetSchema,
simple_snippet: SimpleSnippetSchema,
newsletter_snippet: SubmitFormSchema,
fxa_signup_snippet: SubmitFormSchema,
send_to_device_snippet: SubmitFormSchema,
};
describe("Firefox 57 compatibility test", () => {
Object.keys(expectedValues).forEach(template => {
describe(template, () => {
const schema = SnippetsSchemas[template];
it(`should have a schema for ${template}`, () => {
assert.ok(schema);
});
it(`should validate with the schema for ${template}`, () => {
assert.jsonSchema(expectedValues[template], schema);
});
});
});
});

Просмотреть файл

@ -0,0 +1,125 @@
/**
* IMPORTANT NOTE!!!
*
* Please DO NOT introduce breaking changes file without contacting snippets endpoint engineers
* and changing the schema version to reflect a breaking change.
*
*/
const DATA_URI_IMAGE = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
export const expectedValues = {
// Simple Snippet (https://github.com/mozmeao/snippets/blob/master/activity-stream/simple-snippet.html)
simple_snippet: {
icon: DATA_URI_IMAGE,
button_label: "Click me",
button_url: "https://mozilla.org",
button_background_color: "#FF0000",
button_color: "#FFFFFF",
text: "Hello world",
title: "Hi!",
title_icon: DATA_URI_IMAGE,
tall: true,
},
// FXA Snippet (https://github.com/mozmeao/snippets/blob/master/activity-stream/fxa.html)
fxa_signup_snippet: {
scene1_icon: DATA_URI_IMAGE,
scene1_button_label: "Click me",
scene1_button_background_color: "#FF0000",
scene1_button_color: "#FFFFFF",
scene1_text: "Hello <em>world</em>",
scene1_title: "Hi!",
scene1_title_icon: DATA_URI_IMAGE,
scene2_text: "Second scene",
scene2_title: "Second scene title",
scene2_email_placeholder_text: "Email here",
scene2_button_label: "Sign Me Up",
scene2_dismiss_button_text: "Dismiss",
utm_campaign: "snippets123",
utm_term: "123term",
},
// Send To Device Snippet (https://github.com/mozmeao/snippets/blob/master/activity-stream/send-to-device.html)
send_to_device_snippet: {
include_sms: true,
locale: "de",
country: "DE",
message_id_sms: "foo",
message_id_email: "foo",
scene1_button_background_color: "#FF0000",
scene1_button_color: "#FFFFFF",
scene1_button_label: "Click me",
scene1_icon: DATA_URI_IMAGE,
scene1_text: "Hello world",
scene1_title: "Hi!",
scene1_title_icon: DATA_URI_IMAGE,
scene2_button_label: "Sign Me Up",
scene2_disclaimer_html: "Hello <em>world</em>",
scene2_dismiss_button_text: "Dismiss",
scene2_icon: DATA_URI_IMAGE,
scene2_input_placeholder: "Email here",
scene2_text: "Second scene",
scene2_title: "Second scene title",
error_text: "error",
success_text: "all good",
success_title: "Ok!",
},
// Newsletter Snippet (https://github.com/mozmeao/snippets/blob/master/activity-stream/newsletter-subscribe.html)
newsletter_snippet: {
scene1_icon: DATA_URI_IMAGE,
scene1_button_label: "Click me",
scene1_button_background_color: "#FF0000",
scene1_button_color: "#FFFFFF",
scene1_text: "Hello world",
scene1_title: "Hi!",
scene1_title_icon: DATA_URI_IMAGE,
scene2_text: "Second scene",
scene2_title: "Second scene title",
scene2_newsletter: "foo",
scene2_email_placeholder_text: "Email here",
scene2_button_label: "Sign Me Up",
scene2_privacy_html: "Hello <em>world</em>",
scene2_dismiss_button_text: "Dismiss",
locale: "de",
error_text: "error",
success_text: "all good",
},
// EOY Snippet (https://github.com/mozmeao/snippets/blob/master/activity-stream/mofo-eoy-2017.html)
eoy_snippet: {
block_button_text: "Block",
donation_form_url: "https://donate.mozilla.org/",
text: "Big corporations want to restrict how we access the web. Fake news is making it harder for us to find the truth. Online bullies are silencing inspired voices. The not-for-profit Mozilla Foundation fights for a healthy internet with programs like our Tech Policy Fellowships and Internet Health Report; will you donate today?",
icon: DATA_URI_IMAGE,
button_label: "Donate",
monthly_checkbox_label_text: "Make my donation monthly",
button_background_color: "#0060DF",
button_color: "#FFFFFF",
background_color: "#FFFFFF",
text_color: "#000000",
highlight_color: "#FFE900",
locale: "en-US",
currency_code: "usd",
donation_amount_first: 50,
donation_amount_second: 25,
donation_amount_third: 10,
donation_amount_fourth: 3,
selected_button: "donation_amount_second",
test: "bold",
},
};

Просмотреть файл

@ -0,0 +1,117 @@
import {EOYSnippet} from "content-src/asrouter/templates/EOYSnippet/EOYSnippet";
import {GlobalOverrider} from "test/unit/utils";
import {mount} from "enzyme";
import React from "react";
import schema from "content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json";
const DEFAULT_CONTENT = {
text: "foo",
donation_amount_first: 50,
donation_amount_second: 25,
donation_amount_third: 10,
donation_amount_fourth: 5,
donation_form_url: "https://submit.form",
button_label: "Donate",
currency_code: "usd",
};
describe("EOYSnippet", () => {
let sandbox;
let wrapper;
/**
* mountAndCheckProps - Mounts a EOYSnippet with DEFAULT_CONTENT extended with any props
* passed in the content param and validates props against the schema.
* @param {obj} content Object containing custom message content (e.g. {text, icon, title})
* @returns enzyme wrapper for EOYSnippet
*/
function mountAndCheckProps(content = {}, provider = "test-provider") {
const props = {
content: Object.assign({}, DEFAULT_CONTENT, content),
provider,
onAction: sandbox.stub(),
onBlock: sandbox.stub(),
};
assert.jsonSchema(props.content, schema);
return mount(<EOYSnippet {...props} />);
}
beforeEach(() => {
sandbox = sinon.sandbox.create();
wrapper = mountAndCheckProps();
});
afterEach(() => {
sandbox.restore();
});
it("should render 4 donation options", () => {
assert.lengthOf(wrapper.find("input[type='radio']"), 4);
});
it("should select the second donation option", () => {
wrapper = mountAndCheckProps({selected_button: "donation_amount_second"});
assert.propertyVal(wrapper.find("input[type='radio']").get(1).props, "defaultChecked", true);
});
it("should set frequency value to monthly", () => {
const form = wrapper.find("form").instance();
assert.equal(form.querySelector("[name='frequency']").value, "single");
form.querySelector("#monthly-checkbox").checked = true;
wrapper.find("form").simulate("submit");
assert.equal(form.querySelector("[name='frequency']").value, "monthly");
});
it("should block after submitting the form", () => {
const onBlockStub = sandbox.stub();
wrapper.setProps({onBlock: onBlockStub});
wrapper.find("form").simulate("submit");
assert.calledOnce(onBlockStub);
});
it("should not block if do_not_autoblock is true", () => {
const onBlockStub = sandbox.stub();
wrapper = mountAndCheckProps({do_not_autoblock: true});
wrapper.setProps({onBlock: onBlockStub});
wrapper.find("form").simulate("submit");
assert.notCalled(onBlockStub);
});
describe("locale", () => {
let stub;
let globals;
beforeEach(() => {
globals = new GlobalOverrider();
stub = sandbox.stub().returns({format: () => {}});
globals = new GlobalOverrider();
globals.set({"Intl": {NumberFormat: stub}});
});
afterEach(() => {
globals.restore();
});
it("should use content.locale for Intl", () => {
// triggers component rendering and calls the function we're testing
wrapper.setProps({content: {locale: "locale-foo"}});
assert.calledOnce(stub);
assert.calledWithExactly(stub, "locale-foo", sinon.match.object);
});
it("should use navigator.language as locale fallback", () => {
// triggers component rendering and calls the function we're testing
wrapper.setProps({content: {locale: null}});
assert.calledOnce(stub);
assert.calledWithExactly(stub, navigator.language, sinon.match.object);
});
});
});

Просмотреть файл

@ -173,6 +173,21 @@ describe("SubmitFormSnippet", () => {
assert.equal(wrapper.state().signupSubmitted, true);
assert.notCalled(onBlockStub);
});
it("should not block if do_not_autoblock is true", async () => {
sandbox.stub(window, "fetch").resolves(fetchOk);
wrapper = mountAndCheckProps({
scene1_text: "bar",
scene2_email_placeholder_text: "Email",
scene2_text: "signup",
do_not_autoblock: true,
});
wrapper.setState({expanded: true});
await wrapper.instance().handleSubmit({preventDefault: sandbox.stub()});
assert.equal(wrapper.state().signupSuccess, true);
assert.equal(wrapper.state().signupSubmitted, true);
assert.notCalled(onBlockStub);
});
it("should send user telemetry if submission failed", async () => {
sandbox.stub(window, "fetch").resolves(fetchFail);
wrapper.setState({expanded: true});

Просмотреть файл

@ -992,6 +992,7 @@ describe("TelemetryFeed", () => {
home_url_category: "other",
newtab_url_category: "other",
});
assert.validate(sendEvent.firstCall.args[0], UserEventPing);
});
it("should not send an event if neither about:{home,newtab} are set to custom URL", async () => {
instance._prefs.set(TELEMETRY_PREF, true);

Просмотреть файл

@ -140,6 +140,7 @@ const TEST_GLOBAL = {
getPrefType() {},
clearUserPref() {},
getStringPref() {},
setStringPref() {},
getIntPref() {},
getBoolPref() {},
setBoolPref() {},