Backed out changeset 2cb4cc20ea9d (bug 1569306) for gecko decision bustage CLOSED TREE

This commit is contained in:
Bogdan Tara 2019-07-29 09:46:16 +03:00
Родитель 97c5d52826
Коммит dfedc5dd8a
41 изменённых файлов: 1920 добавлений и 2486 удалений

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

@ -44,7 +44,6 @@ module.exports = {
// These files use fluent-dom to insert content // These files use fluent-dom to insert content
"files": [ "files": [
"content-src/asrouter/templates/OnboardingMessage/**", "content-src/asrouter/templates/OnboardingMessage/**",
"content-src/asrouter/templates/FirstRun/**",
"content-src/asrouter/templates/Trailhead/**", "content-src/asrouter/templates/Trailhead/**",
"content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx", "content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx",
"content-src/components/TopSites/**", "content-src/components/TopSites/**",

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

@ -1,2 +1,2 @@
# flod as main contact for string changes # flod as main contact for string changes
locales-src/ @flodolo locales-src/en-US/strings.properties @flodolo

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

@ -8,19 +8,17 @@ import { generateBundles } from "./rich-text-strings";
import { ImpressionsWrapper } from "./components/ImpressionsWrapper/ImpressionsWrapper"; import { ImpressionsWrapper } from "./components/ImpressionsWrapper/ImpressionsWrapper";
import { LocalizationProvider } from "fluent-react"; import { LocalizationProvider } from "fluent-react";
import { NEWTAB_DARK_THEME } from "content-src/lib/constants"; import { NEWTAB_DARK_THEME } from "content-src/lib/constants";
import { OnboardingMessage } from "./templates/OnboardingMessage/OnboardingMessage";
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { ReturnToAMO } from "./templates/ReturnToAMO/ReturnToAMO";
import { SnippetsTemplates } from "./templates/template-manifest"; import { SnippetsTemplates } from "./templates/template-manifest";
import { FirstRun } from "./templates/FirstRun/FirstRun"; import { StartupOverlay } from "./templates/StartupOverlay/StartupOverlay";
import { Trailhead } from "./templates/Trailhead/Trailhead";
const INCOMING_MESSAGE_NAME = "ASRouter:parent-to-child"; const INCOMING_MESSAGE_NAME = "ASRouter:parent-to-child";
const OUTGOING_MESSAGE_NAME = "ASRouter:child-to-parent"; const OUTGOING_MESSAGE_NAME = "ASRouter:child-to-parent";
const TEMPLATES_ABOVE_PAGE = [ const TEMPLATES_ABOVE_PAGE = ["trailhead"];
"trailhead",
"fxa_overlay",
"return_to_amo_overlay",
];
const FIRST_RUN_TEMPLATES = TEMPLATES_ABOVE_PAGE;
const TEMPLATES_BELOW_SEARCH = ["simple_below_search_snippet"]; const TEMPLATES_BELOW_SEARCH = ["simple_below_search_snippet"];
export const ASRouterUtils = { export const ASRouterUtils = {
@ -48,6 +46,9 @@ export const ASRouterUtils = {
dismissById(id) { dismissById(id) {
ASRouterUtils.sendMessage({ type: "DISMISS_MESSAGE_BY_ID", data: { id } }); ASRouterUtils.sendMessage({ type: "DISMISS_MESSAGE_BY_ID", data: { id } });
}, },
dismissBundle(bundle) {
ASRouterUtils.sendMessage({ type: "DISMISS_BUNDLE", data: { bundle } });
},
executeAction(button_action) { executeAction(button_action) {
ASRouterUtils.sendMessage({ ASRouterUtils.sendMessage({
type: "USER_ACTION", type: "USER_ACTION",
@ -108,7 +109,7 @@ export class ASRouterUISurface extends React.PureComponent {
this.sendClick = this.sendClick.bind(this); this.sendClick = this.sendClick.bind(this);
this.sendImpression = this.sendImpression.bind(this); this.sendImpression = this.sendImpression.bind(this);
this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this); this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this);
this.state = { message: {} }; this.state = { message: {}, bundle: {} };
if (props.document) { if (props.document) {
this.headerPortal = props.document.getElementById( this.headerPortal = props.document.getElementById(
"header-asrouter-container" "header-asrouter-container"
@ -120,10 +121,14 @@ export class ASRouterUISurface extends React.PureComponent {
} }
sendUserActionTelemetry(extraProps = {}) { sendUserActionTelemetry(extraProps = {}) {
const { message } = this.state; const { message, bundle } = this.state;
const eventType = `${message.provider}_user_event`; if (!message && !extraProps.message_id) {
throw new Error(`You must provide a message_id for bundled messages`);
}
// snippets_user_event, onboarding_user_event
const eventType = `${message.provider || bundle.provider}_user_event`;
ASRouterUtils.sendTelemetry({ ASRouterUtils.sendTelemetry({
message_id: message.id, message_id: message.id || extraProps.message_id,
source: extraProps.id, source: extraProps.id,
action: eventType, action: eventType,
...extraProps, ...extraProps,
@ -175,6 +180,26 @@ export class ASRouterUISurface extends React.PureComponent {
return () => ASRouterUtils.dismissById(id); return () => ASRouterUtils.dismissById(id);
} }
dismissBundle(bundle) {
return () => {
ASRouterUtils.dismissBundle(bundle);
this.sendUserActionTelemetry({
event: "DISMISS",
id: "onboarding-cards",
message_id: bundle.map(m => m.id).join(","),
// Passing the action because some bundles (Trailhead) don't have a provider set
action: "onboarding_user_event",
});
};
}
triggerOnboarding() {
ASRouterUtils.sendMessage({
type: "TRIGGER",
data: { trigger: { id: "showOnboarding" } },
});
}
clearMessage(id) { clearMessage(id) {
if (id === this.state.message.id) { if (id === this.state.message.id) {
this.setState({ message: {} }); this.setState({ message: {} });
@ -188,6 +213,9 @@ export class ASRouterUISurface extends React.PureComponent {
case "SET_MESSAGE": case "SET_MESSAGE":
this.setState({ message: action.data }); this.setState({ message: action.data });
break; break;
case "SET_BUNDLED_MESSAGES":
this.setState({ bundle: action.data });
break;
case "CLEAR_MESSAGE": case "CLEAR_MESSAGE":
this.clearMessage(action.data.id); this.clearMessage(action.data.id);
break; break;
@ -196,8 +224,13 @@ export class ASRouterUISurface extends React.PureComponent {
this.setState({ message: {} }); this.setState({ message: {} });
} }
break; break;
case "CLEAR_BUNDLE":
if (this.state.bundle.bundle) {
this.setState({ bundle: {} });
}
break;
case "CLEAR_ALL": case "CLEAR_ALL":
this.setState({ message: {} }); this.setState({ message: {}, bundle: {} });
break; break;
case "AS_ROUTER_TARGETING_UPDATE": case "AS_ROUTER_TARGETING_UPDATE":
action.data.forEach(id => this.clearMessage(id)); action.data.forEach(id => this.clearMessage(id));
@ -238,11 +271,18 @@ export class ASRouterUISurface extends React.PureComponent {
} }
renderSnippets() { renderSnippets() {
const { message } = this.state; if (
if (!SnippetsTemplates[message.template]) { this.state.bundle.template === "onboarding" ||
[
"fxa_overlay",
"return_to_amo_overlay",
"trailhead",
"whatsnew_panel_message",
].includes(this.state.message.template)
) {
return null; return null;
} }
const SnippetComponent = SnippetsTemplates[message.template]; const SnippetComponent = SnippetsTemplates[this.state.message.template];
const { content } = this.state.message; const { content } = this.state.message;
return ( return (
@ -269,6 +309,70 @@ export class ASRouterUISurface extends React.PureComponent {
); );
} }
renderOnboarding() {
if (this.state.bundle.template === "onboarding") {
return (
<OnboardingMessage
{...this.state.bundle}
UISurface="NEWTAB_OVERLAY"
onAction={ASRouterUtils.executeAction}
onDismissBundle={this.dismissBundle(this.state.bundle.bundle)}
sendUserActionTelemetry={this.sendUserActionTelemetry}
/>
);
}
return null;
}
renderFirstRunOverlay() {
const { message } = this.state;
if (message.template === "fxa_overlay") {
global.document.body.classList.add("fxa");
return (
<StartupOverlay
onReady={this.triggerOnboarding}
onBlock={this.onDismissById(message.id)}
dispatch={this.props.dispatch}
/>
);
} else if (message.template === "return_to_amo_overlay") {
global.document.body.classList.add("amo");
return (
<LocalizationProvider
bundles={generateBundles({ amo_html: message.content.text })}
>
<ReturnToAMO
{...message}
UISurface="NEWTAB_OVERLAY"
onReady={this.triggerOnboarding}
onBlock={this.onDismissById(message.id)}
onAction={ASRouterUtils.executeAction}
sendUserActionTelemetry={this.sendUserActionTelemetry}
/>
</LocalizationProvider>
);
}
return null;
}
renderTrailhead() {
const { message } = this.state;
if (message.template === "trailhead") {
return (
<Trailhead
document={this.props.document}
message={message}
onAction={ASRouterUtils.executeAction}
onDismissBundle={this.dismissBundle(this.state.message.bundle)}
sendUserActionTelemetry={this.sendUserActionTelemetry}
dispatch={this.props.dispatch}
fxaEndpoint={this.props.fxaEndpoint}
/>
);
}
return null;
}
renderPreviewBanner() { renderPreviewBanner() {
if (this.state.message.provider !== "preview") { if (this.state.message.provider !== "preview") {
return null; return null;
@ -282,27 +386,9 @@ export class ASRouterUISurface extends React.PureComponent {
); );
} }
renderFirstRun() {
const { message } = this.state;
if (FIRST_RUN_TEMPLATES.includes(message.template)) {
return (
<FirstRun
document={this.props.document}
message={message}
sendUserActionTelemetry={this.sendUserActionTelemetry}
executeAction={ASRouterUtils.executeAction}
dispatch={this.props.dispatch}
onDismiss={this.onDismissById(this.state.message.id)}
fxaEndpoint={this.props.fxaEndpoint}
/>
);
}
return null;
}
render() { render() {
const { message } = this.state; const { message, bundle } = this.state;
if (!message.id) { if (!message.id && !bundle.template) {
return null; return null;
} }
const shouldRenderBelowSearch = TEMPLATES_BELOW_SEARCH.includes( const shouldRenderBelowSearch = TEMPLATES_BELOW_SEARCH.includes(
@ -321,7 +407,9 @@ export class ASRouterUISurface extends React.PureComponent {
ReactDOM.createPortal( ReactDOM.createPortal(
<> <>
{this.renderPreviewBanner()} {this.renderPreviewBanner()}
{this.renderFirstRun()} {this.renderTrailhead()}
{this.renderFirstRunOverlay()}
{this.renderOnboarding()}
{this.renderSnippets()} {this.renderSnippets()}
</>, </>,
shouldRenderInHeader ? this.headerPortal : this.footerPortal shouldRenderInHeader ? this.headerPortal : this.footerPortal

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

@ -1,227 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from "react";
import { Interrupt } from "./Interrupt";
import { Triplets } from "./Triplets";
import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
import { addUtmParams } from "./addUtmParams";
export const FLUENT_FILES = [
"branding/brand.ftl",
"browser/branding/brandings.ftl",
"browser/branding/sync-brand.ftl",
"browser/newtab/onboarding.ftl",
];
export const helpers = {
selectInterruptAndTriplets(message = {}) {
const hasInterrupt = Boolean(message.content);
const hasTriplets = Boolean(message.bundle && message.bundle.length);
const UTMTerm = message.utm_term || "";
return {
hasTriplets,
hasInterrupt,
interrupt: hasInterrupt ? message : null,
triplets: hasTriplets ? message.bundle : null,
UTMTerm,
};
},
addFluent(document) {
FLUENT_FILES.forEach(file => {
const link = document.head.appendChild(document.createElement("link"));
link.href = file;
link.rel = "localization";
});
},
async fetchFlowParams({ fxaEndpoint, UTMTerm, dispatch, setFlowParams }) {
try {
const url = new URL(
`${fxaEndpoint}/metrics-flow?entrypoint=activity-stream-firstrun&form_type=email`
);
addUtmParams(url, UTMTerm);
const response = await fetch(url, { credentials: "omit" });
if (response.status === 200) {
const { deviceId, flowId, flowBeginTime } = await response.json();
setFlowParams({ deviceId, flowId, flowBeginTime });
} else {
dispatch(
ac.OnlyToMain({
type: at.TELEMETRY_UNDESIRED_EVENT,
data: {
event: "FXA_METRICS_FETCH_ERROR",
value: response.status,
},
})
);
}
} catch (error) {
dispatch(
ac.OnlyToMain({
type: at.TELEMETRY_UNDESIRED_EVENT,
data: { event: "FXA_METRICS_ERROR" },
})
);
}
},
};
export class FirstRun extends React.PureComponent {
constructor(props) {
super(props);
this.didLoadFlowParams = false;
this.state = {
prevMessage: undefined,
hasInterrupt: false,
hasTriplets: false,
interrupt: undefined,
triplets: undefined,
isInterruptVisible: false,
isTripletsContainerVisible: false,
isTripletsContentVisible: false,
UTMTerm: "",
flowParams: undefined,
};
this.closeInterrupt = this.closeInterrupt.bind(this);
this.closeTriplets = this.closeTriplets.bind(this);
helpers.addFluent(this.props.document);
}
static getDerivedStateFromProps(props, state) {
const { message } = props;
if (message && message.id !== state.prevMessageId) {
const {
hasTriplets,
hasInterrupt,
interrupt,
triplets,
UTMTerm,
} = helpers.selectInterruptAndTriplets(message);
return {
prevMessageId: message.id,
hasInterrupt,
hasTriplets,
interrupt,
triplets,
isInterruptVisible: hasInterrupt,
isTripletsContainerVisible: hasTriplets,
isTripletsContentVisible: !(hasInterrupt || !hasTriplets),
UTMTerm,
};
}
return null;
}
fetchFlowParams() {
const { fxaEndpoint, dispatch } = this.props;
const { UTMTerm } = this.state;
if (fxaEndpoint && UTMTerm && !this.didLoadFlowParams) {
this.didLoadFlowParams = true;
helpers.fetchFlowParams({
fxaEndpoint,
UTMTerm,
dispatch,
setFlowParams: flowParams => this.setState({ flowParams }),
});
}
}
removeHideMain() {
if (!this.state.hasInterrupt) {
// We need to remove hide-main since we should show it underneath everything that has rendered
this.props.document.body.classList.remove("hide-main", "welcome");
}
}
componentDidMount() {
this.fetchFlowParams();
this.removeHideMain();
}
componentDidUpdate() {
// In case we didn't have FXA info immediately, try again when we receive it.
this.fetchFlowParams();
this.removeHideMain();
}
closeInterrupt() {
this.setState(prevState => ({
isInterruptVisible: false,
isTripletsContainerVisible: prevState.hasTriplets,
isTripletsContentVisible: prevState.hasTriplets,
}));
}
closeTriplets() {
this.setState({ isTripletsContainerVisible: false });
}
render() {
const { props } = this;
const {
sendUserActionTelemetry,
fxaEndpoint,
dispatch,
executeAction,
} = props;
const {
interrupt,
triplets,
isInterruptVisible,
isTripletsContainerVisible,
isTripletsContentVisible,
hasTriplets,
UTMTerm,
flowParams,
} = this.state;
return (
<>
{isInterruptVisible ? (
<Interrupt
document={props.document}
message={interrupt}
onNextScene={this.closeInterrupt}
UTMTerm={UTMTerm}
sendUserActionTelemetry={sendUserActionTelemetry}
dispatch={dispatch}
flowParams={flowParams}
onDismiss={this.closeInterrupt}
fxaEndpoint={fxaEndpoint}
/>
) : null}
{hasTriplets ? (
<Triplets
document={props.document}
cards={triplets}
showCardPanel={isTripletsContainerVisible}
showContent={isTripletsContentVisible}
hideContainer={this.closeTriplets}
sendUserActionTelemetry={sendUserActionTelemetry}
UTMTerm={`${UTMTerm}-card`}
flowParams={flowParams}
onAction={executeAction}
/>
) : null}
</>
);
}
}

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

@ -1,67 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from "react";
import { Trailhead } from "../Trailhead/Trailhead";
import { ReturnToAMO } from "../ReturnToAMO/ReturnToAMO";
import { StartupOverlay } from "../StartupOverlay/StartupOverlay";
import { LocalizationProvider } from "fluent-react";
import { generateBundles } from "../../rich-text-strings";
export class Interrupt extends React.PureComponent {
render() {
const {
onDismiss,
onNextScene,
message,
sendUserActionTelemetry,
executeAction,
dispatch,
fxaEndpoint,
UTMTerm,
flowParams,
} = this.props;
switch (message.template) {
case "return_to_amo_overlay":
return (
<LocalizationProvider
bundles={generateBundles({ amo_html: message.content.text })}
>
<ReturnToAMO
{...message}
UISurface="NEWTAB_OVERLAY"
onBlock={onDismiss}
onAction={executeAction}
sendUserActionTelemetry={sendUserActionTelemetry}
/>
</LocalizationProvider>
);
case "fxa_overlay":
return (
<StartupOverlay
onBlock={onDismiss}
dispatch={dispatch}
fxa_endpoint={fxaEndpoint}
/>
);
case "trailhead":
return (
<Trailhead
document={this.props.document}
message={message}
onNextScene={onNextScene}
onAction={executeAction}
sendUserActionTelemetry={sendUserActionTelemetry}
dispatch={dispatch}
fxaEndpoint={fxaEndpoint}
UTMTerm={UTMTerm}
flowParams={flowParams}
/>
);
default:
throw new Error(`${message.template} is not a valid FirstRun message`);
}
}
}

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

@ -1,91 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from "react";
import { OnboardingCard } from "../../templates/OnboardingMessage/OnboardingMessage";
import { addUtmParams } from "./addUtmParams";
export class Triplets extends React.PureComponent {
constructor(props) {
super(props);
this.onCardAction = this.onCardAction.bind(this);
this.onHideContainer = this.onHideContainer.bind(this);
}
componentWillMount() {
global.document.body.classList.add("inline-onboarding");
}
componentWillUnmount() {
this.props.document.body.classList.remove("inline-onboarding");
}
onCardAction(action) {
let actionUpdates = {};
const { flowParams, UTMTerm } = this.props;
if (action.type === "OPEN_URL") {
let url = new URL(action.data.args);
addUtmParams(url, UTMTerm);
if (action.addFlowParams) {
url.searchParams.append("device_id", flowParams.deviceId);
url.searchParams.append("flow_id", flowParams.flowId);
url.searchParams.append("flow_begin_time", flowParams.flowBeginTime);
}
actionUpdates = { data: { ...action.data, args: url.toString() } };
}
this.props.onAction({ ...action, ...actionUpdates });
}
onHideContainer() {
const { sendUserActionTelemetry, cards, hideContainer } = this.props;
hideContainer();
sendUserActionTelemetry({
event: "DISMISS",
id: "onboarding-cards",
message_id: cards.map(m => m.id).join(","),
action: "onboarding_user_event",
});
}
render() {
const {
cards,
showCardPanel,
showContent,
sendUserActionTelemetry,
} = this.props;
return (
<div
className={`trailheadCards ${showCardPanel ? "expanded" : "collapsed"}`}
>
<div className="trailheadCardsInner" aria-hidden={!showContent}>
<h1 data-l10n-id="onboarding-welcome-header" />
<div className={`trailheadCardGrid${showContent ? " show" : ""}`}>
{cards.map(card => (
<OnboardingCard
key={card.id}
className="trailheadCard"
sendUserActionTelemetry={sendUserActionTelemetry}
onAction={this.onCardAction}
UISurface="TRAILHEAD"
{...card}
/>
))}
</div>
{showCardPanel && (
<button
className="icon icon-dismiss"
onClick={this.onHideContainer}
data-l10n-id="onboarding-cards-dismiss"
/>
)}
</div>
</div>
);
}
}

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

@ -1,27 +0,0 @@
/* 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/. */
const BASE_PARAMS = {
utm_source: "activity-stream",
utm_campaign: "firstrun",
utm_medium: "referral",
};
/**
* Takes in a url as a string or URL object and returns a URL object with the
* utm_* parameters added to it. If a URL object is passed in, the paraemeters
* are added to it (the return value can be ignored in that case as it's the
* same object).
*/
export function addUtmParams(url, utmTerm) {
let returnUrl = url;
if (typeof returnUrl === "string") {
returnUrl = new URL(url);
}
Object.keys(BASE_PARAMS).forEach(key => {
returnUrl.searchParams.append(key, BASE_PARAMS[key]);
});
returnUrl.searchParams.append("utm_term", utmTerm);
return returnUrl;
}

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

@ -2,8 +2,15 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file, * 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/. */ * You can obtain one at http://mozilla.org/MPL/2.0/. */
import { ModalOverlay } from "../../components/ModalOverlay/ModalOverlay";
import React from "react"; import React from "react";
const FLUENT_FILES = [
"branding/brand.ftl",
"browser/branding/sync-brand.ftl",
"browser/newtab/onboarding.ftl",
];
export class OnboardingCard extends React.PureComponent { export class OnboardingCard extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@ -50,3 +57,33 @@ export class OnboardingCard extends React.PureComponent {
); );
} }
} }
export class OnboardingMessage extends React.PureComponent {
componentWillMount() {
FLUENT_FILES.forEach(file => {
const link = document.head.appendChild(document.createElement("link"));
link.href = file;
link.rel = "localization";
});
}
render() {
const { props } = this;
const { button_label, header } = props.extraTemplateStrings;
return (
<ModalOverlay {...props} button_label={button_label} title={header}>
<div className="onboardingMessageContainer">
{props.bundle.map(message => (
<OnboardingCard
key={message.id}
sendUserActionTelemetry={props.sendUserActionTelemetry}
onAction={props.onAction}
UISurface={props.UISurface}
{...message}
/>
))}
</div>
</ModalOverlay>
);
}
}

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

@ -1,3 +1,20 @@
.onboardingMessageContainer {
display: grid;
grid-column-gap: 21px;
grid-template-columns: auto auto auto;
padding-left: 30px;
padding-right: 30px;
min-height: 500px;
// at 850px, the cards go from vertical layout to horizontal layout
@media(max-width: 850px) {
grid-template-columns: none;
grid-template-rows: auto auto auto;
padding-left: 110px;
padding-right: 110px;
}
}
.onboardingMessage { .onboardingMessage {
height: 340px; height: 340px;
text-align: center; text-align: center;

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

@ -15,11 +15,8 @@ export class ReturnToAMO extends React.PureComponent {
this.onBlockButton = this.onBlockButton.bind(this); this.onBlockButton = this.onBlockButton.bind(this);
} }
componentWillMount() {
global.document.body.classList.add("amo");
}
componentDidMount() { componentDidMount() {
this.props.onReady();
this.props.sendUserActionTelemetry({ this.props.sendUserActionTelemetry({
event: "IMPRESSION", event: "IMPRESSION",
id: this.props.UISurface, id: this.props.UISurface,

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

@ -2,15 +2,23 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file, * 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/. */ * You can obtain one at http://mozilla.org/MPL/2.0/. */
import { actionCreators as ac } from "common/Actions.jsm"; import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
import { connect } from "react-redux";
import React from "react"; import React from "react";
export class StartupOverlay extends React.PureComponent { const FLUENT_FILES = [
"branding/brand.ftl",
"browser/branding/sync-brand.ftl",
"browser/newtab/onboarding.ftl",
];
export class _StartupOverlay extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.onInputChange = this.onInputChange.bind(this); this.onInputChange = this.onInputChange.bind(this);
this.onSubmit = this.onSubmit.bind(this); this.onSubmit = this.onSubmit.bind(this);
this.clickSkip = this.clickSkip.bind(this); this.clickSkip = this.clickSkip.bind(this);
this.initScene = this.initScene.bind(this);
this.removeOverlay = this.removeOverlay.bind(this); this.removeOverlay = this.removeOverlay.bind(this);
this.onInputInvalid = this.onInputInvalid.bind(this); this.onInputInvalid = this.onInputInvalid.bind(this);
@ -18,20 +26,71 @@ export class StartupOverlay extends React.PureComponent {
"utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-control"; "utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-control";
this.state = { this.state = {
show: false,
emailInput: "", emailInput: "",
overlayRemoved: false,
deviceId: "",
flowId: "",
flowBeginTime: 0,
}; };
this.didFetch = false;
} }
componentWillMount() { async componentWillUpdate() {
global.document.body.classList.add("fxa"); if (this.props.fxa_endpoint && !this.didFetch) {
try {
this.didFetch = true;
const fxaParams = "entrypoint=activity-stream-firstrun&form_type=email";
const response = await fetch(
`${this.props.fxa_endpoint}/metrics-flow?${fxaParams}&${
this.utmParams
}`,
{ credentials: "omit" }
);
if (response.status === 200) {
const { deviceId, flowId, flowBeginTime } = await response.json();
this.setState({ deviceId, flowId, flowBeginTime });
} else {
this.props.dispatch(
ac.OnlyToMain({
type: at.TELEMETRY_UNDESIRED_EVENT,
data: {
event: "FXA_METRICS_FETCH_ERROR",
value: response.status,
},
})
);
}
} catch (error) {
this.props.dispatch(
ac.OnlyToMain({
type: at.TELEMETRY_UNDESIRED_EVENT,
data: { event: "FXA_METRICS_ERROR" },
})
);
}
}
}
async componentWillMount() {
FLUENT_FILES.forEach(file => {
const link = document.head.appendChild(document.createElement("link"));
link.href = file;
link.rel = "localization";
});
await this.componentWillUpdate(this.props);
} }
componentDidMount() { componentDidMount() {
this.initScene();
}
initScene() {
// Timeout to allow the scene to render once before attaching the attribute // Timeout to allow the scene to render once before attaching the attribute
// to trigger the animation. // to trigger the animation.
setTimeout(() => { setTimeout(() => {
this.setState({ show: true }); this.setState({ show: true });
this.props.onReady();
}, 10); }, 10);
} }
@ -39,11 +98,11 @@ export class StartupOverlay extends React.PureComponent {
window.removeEventListener("visibilitychange", this.removeOverlay); window.removeEventListener("visibilitychange", this.removeOverlay);
document.body.classList.remove("hide-main", "fxa"); document.body.classList.remove("hide-main", "fxa");
this.setState({ show: false }); this.setState({ show: false });
this.props.onBlock();
setTimeout(() => { setTimeout(() => {
// Allow scrolling and fully remove overlay after animation finishes. // Allow scrolling and fully remove overlay after animation finishes.
this.props.onBlock();
document.body.classList.remove("welcome"); document.body.classList.remove("welcome");
this.setState({ overlayRemoved: true });
}, 400); }, 400);
} }
@ -73,9 +132,7 @@ export class StartupOverlay extends React.PureComponent {
* Report to telemetry additional information about the form submission. * Report to telemetry additional information about the form submission.
*/ */
_getFormInfo() { _getFormInfo() {
const value = { const value = { has_flow_params: this.state.flowId.length > 0 };
has_flow_params: this.props.flowParams.flowId.length > 0,
};
return { value }; return { value };
} }
@ -88,6 +145,12 @@ export class StartupOverlay extends React.PureComponent {
} }
render() { render() {
// When skipping the onboarding tour we show AS but we are still on
// about:welcome, prop.isFirstrun is true and StartupOverlay is rendered
if (this.state.overlayRemoved) {
return null;
}
return ( return (
<div className={`overlay-wrapper ${this.state.show ? "show" : ""}`}> <div className={`overlay-wrapper ${this.state.show ? "show" : ""}`}>
<div className="background" /> <div className="background" />
@ -150,17 +213,13 @@ export class StartupOverlay extends React.PureComponent {
<input <input
name="device_id" name="device_id"
type="hidden" type="hidden"
value={this.props.flowParams.deviceId} value={this.state.deviceId}
/>
<input
name="flow_id"
type="hidden"
value={this.props.flowParams.flowId}
/> />
<input name="flow_id" type="hidden" value={this.state.flowId} />
<input <input
name="flow_begin_time" name="flow_begin_time"
type="hidden" type="hidden"
value={this.props.flowParams.flowBeginTime} value={this.state.flowBeginTime}
/> />
<span <span
className="error" className="error"
@ -170,7 +229,7 @@ export class StartupOverlay extends React.PureComponent {
className="email-input" className="email-input"
name="email" name="email"
type="email" type="email"
required={true} required="true"
onInvalid={this.onInputInvalid} onInvalid={this.onInputInvalid}
onChange={this.onInputChange} onChange={this.onInputChange}
data-l10n-id="onboarding-sync-form-input" data-l10n-id="onboarding-sync-form-input"
@ -215,6 +274,5 @@ export class StartupOverlay extends React.PureComponent {
} }
} }
StartupOverlay.defaultProps = { const getState = state => ({ fxa_endpoint: state.Prefs.values.fxa_endpoint });
flowParams: { deviceId: "", flowId: "", flowBeginTime: "" }, export const StartupOverlay = connect(getState)(_StartupOverlay);
};

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

@ -2,11 +2,18 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file, * 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/. */ * You can obtain one at http://mozilla.org/MPL/2.0/. */
import { actionCreators as ac } from "common/Actions.jsm"; import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
import { ModalOverlayWrapper } from "../../components/ModalOverlay/ModalOverlay"; import { ModalOverlayWrapper } from "../../components/ModalOverlay/ModalOverlay";
import { addUtmParams } from "../FirstRun/addUtmParams"; import { OnboardingCard } from "../OnboardingMessage/OnboardingMessage";
import React from "react"; import React from "react";
const FLUENT_FILES = [
"branding/brand.ftl",
"browser/branding/brandings.ftl",
"browser/branding/sync-brand.ftl",
"browser/newtab/onboarding.ftl",
];
// From resource://devtools/client/shared/focus.js // From resource://devtools/client/shared/focus.js
const FOCUSABLE_SELECTOR = [ const FOCUSABLE_SELECTOR = [
"a[href]:not([tabindex='-1'])", "a[href]:not([tabindex='-1'])",
@ -22,35 +29,106 @@ export class Trailhead extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.closeModal = this.closeModal.bind(this); this.closeModal = this.closeModal.bind(this);
this.hideCardPanel = this.hideCardPanel.bind(this);
this.onInputChange = this.onInputChange.bind(this); this.onInputChange = this.onInputChange.bind(this);
this.onStartBlur = this.onStartBlur.bind(this); this.onStartBlur = this.onStartBlur.bind(this);
this.onSubmit = this.onSubmit.bind(this); this.onSubmit = this.onSubmit.bind(this);
this.onInputInvalid = this.onInputInvalid.bind(this); this.onInputInvalid = this.onInputInvalid.bind(this);
this.onCardAction = this.onCardAction.bind(this);
this.state = { this.state = {
emailInput: "", emailInput: "",
isModalOpen: true,
showCardPanel: true,
showCards: false,
// The params below are for FxA metrics
deviceId: "",
flowId: "",
flowBeginTime: 0,
}; };
this.fxaMetricsInitialized = false;
} }
get dialog() { get dialog() {
return this.props.document.getElementById("trailheadDialog"); return this.props.document.getElementById("trailheadDialog");
} }
async componentWillMount() {
FLUENT_FILES.forEach(file => {
const link = document.head.appendChild(document.createElement("link"));
link.href = file;
link.rel = "localization";
});
await this.componentWillUpdate(this.props);
}
// Get the fxa data if we don't have it yet from mount or update
async componentWillUpdate(props) {
if (props.fxaEndpoint && !this.fxaMetricsInitialized) {
try {
this.fxaMetricsInitialized = true;
const url = new URL(
`${
props.fxaEndpoint
}/metrics-flow?entrypoint=activity-stream-firstrun&form_type=email`
);
this.addUtmParams(url);
const response = await fetch(url, { credentials: "omit" });
if (response.status === 200) {
const { deviceId, flowId, flowBeginTime } = await response.json();
this.setState({ deviceId, flowId, flowBeginTime });
} else {
props.dispatch(
ac.OnlyToMain({
type: at.TELEMETRY_UNDESIRED_EVENT,
data: {
event: "FXA_METRICS_FETCH_ERROR",
value: response.status,
},
})
);
}
} catch (error) {
props.dispatch(
ac.OnlyToMain({
type: at.TELEMETRY_UNDESIRED_EVENT,
data: { event: "FXA_METRICS_ERROR" },
})
);
}
}
}
componentDidMount() { componentDidMount() {
// We need to remove hide-main since we should show it underneath everything that has rendered // We need to remove hide-main since we should show it underneath everything that has rendered
this.props.document.body.classList.remove("hide-main"); this.props.document.body.classList.remove("hide-main");
// The rest of the page is "hidden" to screen readers when the modal is open // Add inline-onboarding class to disable fixed search header and fixed positioned settings icon
this.props.document this.props.document.body.classList.add("inline-onboarding");
.getElementById("root")
.setAttribute("aria-hidden", "true"); // The rest of the page is "hidden" when the modal is open
// Start with focus in the email input box if (this.props.message.content) {
const input = this.dialog.querySelector("input[name=email]"); this.props.document
if (input) { .getElementById("root")
input.focus(); .setAttribute("aria-hidden", "true");
// Start with focus in the email input box
this.dialog.querySelector("input[name=email]").focus();
} else {
// No modal overlay, let the user scroll and deal them some cards.
this.props.document.body.classList.remove("welcome");
if (this.props.message.includeBundle || this.props.message.cards) {
this.revealCards();
}
} }
} }
componentWillUnmount() {
this.props.document.body.classList.remove("inline-onboarding");
}
onInputChange(e) { onInputChange(e) {
let error = e.target.previousSibling; let error = e.target.previousSibling;
this.setState({ emailInput: e.target.value }); this.setState({ emailInput: e.target.value });
@ -94,7 +172,8 @@ export class Trailhead extends React.PureComponent {
global.removeEventListener("visibilitychange", this.closeModal); global.removeEventListener("visibilitychange", this.closeModal);
this.props.document.body.classList.remove("welcome"); this.props.document.body.classList.remove("welcome");
this.props.document.getElementById("root").removeAttribute("aria-hidden"); this.props.document.getElementById("root").removeAttribute("aria-hidden");
this.props.onNextScene(); this.setState({ isModalOpen: false });
this.revealCards();
// If closeModal() was triggered by a visibilitychange event, the user actually // If closeModal() was triggered by a visibilitychange event, the user actually
// submitted the email form so we don't send a SKIPPED_SIGNIN ping. // submitted the email form so we don't send a SKIPPED_SIGNIN ping.
@ -112,7 +191,7 @@ export class Trailhead extends React.PureComponent {
* Report to telemetry additional information about the form submission. * Report to telemetry additional information about the form submission.
*/ */
_getFormInfo() { _getFormInfo() {
const value = { has_flow_params: this.props.flowParams.flowId.length > 0 }; const value = { has_flow_params: this.state.flowId.length > 0 };
return { value }; return { value };
} }
@ -124,141 +203,234 @@ export class Trailhead extends React.PureComponent {
e.target.focus(); e.target.focus();
} }
hideCardPanel() {
this.setState({ showCardPanel: false });
this.props.onDismissBundle();
}
revealCards() {
this.setState({ showCards: true });
}
/**
* Takes in a url as a string or URL object and returns a URL object with the
* utm_* parameters added to it. If a URL object is passed in, the paraemeters
* are added to it (the return value can be ignored in that case as it's the
* same object).
*/
addUtmParams(url, isCard = false) {
let returnUrl = url;
if (typeof returnUrl === "string") {
returnUrl = new URL(url);
}
returnUrl.searchParams.append("utm_source", "activity-stream");
returnUrl.searchParams.append("utm_campaign", "firstrun");
returnUrl.searchParams.append("utm_medium", "referral");
returnUrl.searchParams.append(
"utm_term",
`${this.props.message.utm_term}${isCard ? "-card" : ""}`
);
return returnUrl;
}
onCardAction(action) {
let actionUpdates = {};
if (action.type === "OPEN_URL") {
let url = new URL(action.data.args);
this.addUtmParams(url, true);
if (action.addFlowParams) {
url.searchParams.append("device_id", this.state.deviceId);
url.searchParams.append("flow_id", this.state.flowId);
url.searchParams.append("flow_begin_time", this.state.flowBeginTime);
}
actionUpdates = { data: { ...action.data, args: url } };
}
this.props.onAction({ ...action, ...actionUpdates });
}
render() { render() {
const { props } = this; const { props } = this;
const { UTMTerm } = props; const { bundle: cards, content, utm_term } = props.message;
const { content } = props.message;
const innerClassName = ["trailhead", content && content.className] const innerClassName = ["trailhead", content && content.className]
.filter(v => v) .filter(v => v)
.join(" "); .join(" ");
return ( return (
<ModalOverlayWrapper <>
innerClassName={innerClassName} {this.state.isModalOpen && content ? (
onClose={this.closeModal} <ModalOverlayWrapper
id="trailheadDialog" innerClassName={innerClassName}
headerId="trailheadHeader" onClose={this.closeModal}
> id="trailheadDialog"
<div className="trailheadInner"> headerId="trailheadHeader"
<div className="trailheadContent">
<h1 data-l10n-id={content.title.string_id} id="trailheadHeader" />
{content.subtitle && (
<p data-l10n-id={content.subtitle.string_id} />
)}
<ul className="trailheadBenefits">
{content.benefits.map(item => (
<li key={item.id} className={item.id}>
<h3 data-l10n-id={item.title.string_id} />
<p data-l10n-id={item.text.string_id} />
</li>
))}
</ul>
<a
className="trailheadLearn"
data-l10n-id={content.learn.text.string_id}
href={addUtmParams(content.learn.url, UTMTerm)}
target="_blank"
rel="noopener noreferrer"
/>
</div>
<div
role="group"
aria-labelledby="joinFormHeader"
aria-describedby="joinFormBody"
className="trailheadForm"
> >
<h3 <div className="trailheadInner">
id="joinFormHeader" <div className="trailheadContent">
data-l10n-id={content.form.title.string_id} <h1
/> data-l10n-id={content.title.string_id}
<p id="joinFormBody" data-l10n-id={content.form.text.string_id} /> id="trailheadHeader"
<form />
method="get" {content.subtitle && (
action={this.props.fxaEndpoint} <p data-l10n-id={content.subtitle.string_id} />
target="_blank" )}
rel="noopener noreferrer" <ul className="trailheadBenefits">
onSubmit={this.onSubmit} {content.benefits.map(item => (
> <li key={item.id} className={item.id}>
<input name="service" type="hidden" value="sync" /> <h3 data-l10n-id={item.title.string_id} />
<input name="action" type="hidden" value="email" /> <p data-l10n-id={item.text.string_id} />
<input name="context" type="hidden" value="fx_desktop_v3" /> </li>
<input ))}
name="entrypoint" </ul>
type="hidden" <a
value="activity-stream-firstrun" className="trailheadLearn"
/> data-l10n-id={content.learn.text.string_id}
<input name="utm_source" type="hidden" value="activity-stream" /> href={this.addUtmParams(content.learn.url)}
<input name="utm_campaign" type="hidden" value="firstrun" /> target="_blank"
<input name="utm_term" type="hidden" value={UTMTerm} /> rel="noopener noreferrer"
<input />
name="device_id" </div>
type="hidden" <div
value={this.props.flowParams.deviceId} role="group"
/> aria-labelledby="joinFormHeader"
<input aria-describedby="joinFormBody"
name="flow_id" className="trailheadForm"
type="hidden"
value={this.props.flowParams.flowId}
/>
<input
name="flow_begin_time"
type="hidden"
value={this.props.flowParams.flowBeginTime}
/>
<input name="style" type="hidden" value="trailhead" />
<p
data-l10n-id="onboarding-join-form-email-error"
className="error"
/>
<input
data-l10n-id={content.form.email.string_id}
name="email"
type="email"
onInvalid={this.onInputInvalid}
onChange={this.onInputChange}
/>
<p
className="trailheadTerms"
data-l10n-id="onboarding-join-form-legal"
> >
<a <h3
data-l10n-name="terms" id="joinFormHeader"
data-l10n-id={content.form.title.string_id}
/>
<p
id="joinFormBody"
data-l10n-id={content.form.text.string_id}
/>
<form
method="get"
action={this.props.fxaEndpoint}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
href={addUtmParams( onSubmit={this.onSubmit}
"https://accounts.firefox.com/legal/terms", >
UTMTerm <input name="service" type="hidden" value="sync" />
)} <input name="action" type="hidden" value="email" />
/> <input name="context" type="hidden" value="fx_desktop_v3" />
<a <input
data-l10n-name="privacy" name="entrypoint"
target="_blank" type="hidden"
rel="noopener noreferrer" value="activity-stream-firstrun"
href={addUtmParams( />
"https://accounts.firefox.com/legal/privacy", <input
UTMTerm name="utm_source"
)} type="hidden"
/> value="activity-stream"
</p> />
<button <input name="utm_campaign" type="hidden" value="firstrun" />
data-l10n-id={content.form.button.string_id} <input name="utm_term" type="hidden" value={utm_term} />
type="submit" <input
/> name="device_id"
</form> type="hidden"
</div> value={this.state.deviceId}
</div> />
<input
name="flow_id"
type="hidden"
value={this.state.flowId}
/>
<input
name="flow_begin_time"
type="hidden"
value={this.state.flowBeginTime}
/>
<input name="style" type="hidden" value="trailhead" />
<p
data-l10n-id="onboarding-join-form-email-error"
className="error"
/>
<input
data-l10n-id={content.form.email.string_id}
name="email"
type="email"
onInvalid={this.onInputInvalid}
onChange={this.onInputChange}
/>
<p
className="trailheadTerms"
data-l10n-id="onboarding-join-form-legal"
>
<a
data-l10n-name="terms"
target="_blank"
rel="noopener noreferrer"
href={this.addUtmParams(
"https://accounts.firefox.com/legal/terms"
)}
/>
<a
data-l10n-name="privacy"
target="_blank"
rel="noopener noreferrer"
href={this.addUtmParams(
"https://accounts.firefox.com/legal/privacy"
)}
/>
</p>
<button
data-l10n-id={content.form.button.string_id}
type="submit"
/>
</form>
</div>
</div>
<button <button
className="trailheadStart" className="trailheadStart"
data-l10n-id={content.skipButton.string_id} data-l10n-id={content.skipButton.string_id}
onBlur={this.onStartBlur} onBlur={this.onStartBlur}
onClick={this.closeModal} onClick={this.closeModal}
/> />
</ModalOverlayWrapper> </ModalOverlayWrapper>
) : null}
{cards && cards.length ? (
<div
className={`trailheadCards ${
this.state.showCardPanel ? "expanded" : "collapsed"
}`}
>
<div
className="trailheadCardsInner"
aria-hidden={!this.state.showCards}
>
<h1 data-l10n-id="onboarding-welcome-header" />
<div
className={`trailheadCardGrid${
this.state.showCards ? " show" : ""
}`}
>
{cards.map(card => (
<OnboardingCard
key={card.id}
className="trailheadCard"
sendUserActionTelemetry={props.sendUserActionTelemetry}
onAction={this.onCardAction}
UISurface="TRAILHEAD"
{...card}
/>
))}
</div>
{this.state.showCardPanel && (
<button
className="icon icon-dismiss"
onClick={this.hideCardPanel}
data-l10n-id="onboarding-cards-dismiss"
/>
)}
</div>
</div>
) : null}
</>
); );
} }
} }
Trailhead.defaultProps = {
flowParams: { deviceId: "", flowId: "", flowBeginTime: "" },
};

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

@ -119,7 +119,6 @@ export class CollapsibleSection extends React.PureComponent {
onKeyPress(event) { onKeyPress(event) {
if (event.key === "Enter" || event.key === " ") { if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
this.onHeaderClick(); this.onHeaderClick();
} }
} }
@ -224,13 +223,16 @@ export class CollapsibleSection extends React.PureComponent {
> >
{this.renderIcon()} {this.renderIcon()}
<FluentOrText message={title} /> <FluentOrText message={title} />
</span>
<span
className="click-target"
role="button"
tabIndex="0"
onKeyPress={this.onKeyPress}
onClick={this.onHeaderClick}
>
{isCollapsible && ( {isCollapsible && (
<span <span
data-l10n-id={
collapsed
? "newtab-section-expand-section-label"
: "newtab-section-collapse-section-label"
}
className={`collapsible-arrow icon ${ className={`collapsible-arrow icon ${
collapsed collapsed
? "icon-arrowhead-forward-small" ? "icon-arrowhead-forward-small"

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

@ -44,16 +44,16 @@ export class ContextMenu extends React.PureComponent {
// Disabling focus on the menu span allows the first tab to focus on the first menu item instead of the wrapper. // Disabling focus on the menu span allows the first tab to focus on the first menu item instead of the wrapper.
return ( return (
// eslint-disable-next-line jsx-a11y/interactive-supports-focus // eslint-disable-next-line jsx-a11y/interactive-supports-focus
<span className="context-menu"> <span
<ul role="menu"
role="menu" className="context-menu"
onClick={this.onClick} onClick={this.onClick}
onKeyDown={this.onClick} onKeyDown={this.onClick}
className="context-menu-list" >
> <ul className="context-menu-list">
{this.props.options.map((option, i) => {this.props.options.map((option, i) =>
option.type === "separator" ? ( option.type === "separator" ? (
<li key={i} className="separator" role="separator" /> <li key={i} className="separator" />
) : ( ) : (
option.type !== "empty" && ( option.type !== "empty" && (
<ContextMenuItem <ContextMenuItem
@ -61,6 +61,7 @@ export class ContextMenu extends React.PureComponent {
option={option} option={option}
hideContext={this.hideContext} hideContext={this.hideContext}
keyboardAccess={this.props.keyboardAccess} keyboardAccess={this.props.keyboardAccess}
tabIndex="0"
/> />
) )
) )
@ -76,7 +77,6 @@ export class ContextMenuItem extends React.PureComponent {
super(props); super(props);
this.onClick = this.onClick.bind(this); this.onClick = this.onClick.bind(this);
this.onKeyDown = this.onKeyDown.bind(this); this.onKeyDown = this.onKeyDown.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.focusFirst = this.focusFirst.bind(this); this.focusFirst = this.focusFirst.bind(this);
} }
@ -129,8 +129,6 @@ export class ContextMenuItem extends React.PureComponent {
this.focusSibling(event.target, event.key); this.focusSibling(event.target, event.key);
break; break;
case "Enter": case "Enter":
case " ":
event.preventDefault();
this.props.hideContext(); this.props.hideContext();
option.onClick(); option.onClick();
break; break;
@ -140,24 +138,15 @@ export class ContextMenuItem extends React.PureComponent {
} }
} }
// Prevents the default behavior of spacebar
// scrolling the page & auto-triggering buttons.
onKeyUp(event) {
if (event.key === " ") {
event.preventDefault();
}
}
render() { render() {
const { option } = this.props; const { option } = this.props;
return ( return (
<li role="presentation" className="context-menu-item"> <li role="menuitem" className="context-menu-item">
<button <button
className={option.disabled ? "disabled" : ""} className={option.disabled ? "disabled" : ""}
role="menuitem" tabIndex="0"
onClick={this.onClick} onClick={this.onClick}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp}
ref={option.first ? this.focusFirst : null} ref={option.first ? this.focusFirst : null}
> >
{option.icon && ( {option.icon && (

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

@ -32,7 +32,7 @@ export class ContextMenuButton extends React.PureComponent {
} }
onKeyDown(event) { onKeyDown(event) {
if (event.key === "Enter" || event.key === " ") { if (event.key === "Enter") {
event.preventDefault(); event.preventDefault();
this.openContextMenu(true, event); this.openContextMenu(true, event);
} }

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

@ -3421,6 +3421,20 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
.submissionStatus .submitStatusTitle { .submissionStatus .submitStatusTitle {
font-size: 20px; } font-size: 20px; }
.onboardingMessageContainer {
display: grid;
grid-column-gap: 21px;
grid-template-columns: auto auto auto;
padding-left: 30px;
padding-right: 30px;
min-height: 500px; }
@media (max-width: 850px) {
.onboardingMessageContainer {
grid-template-columns: none;
grid-template-rows: auto auto auto;
padding-left: 110px;
padding-right: 110px; } }
.onboardingMessage { .onboardingMessage {
height: 340px; height: 340px;
text-align: center; text-align: center;

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

@ -3424,6 +3424,20 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
.submissionStatus .submitStatusTitle { .submissionStatus .submitStatusTitle {
font-size: 20px; } font-size: 20px; }
.onboardingMessageContainer {
display: grid;
grid-column-gap: 21px;
grid-template-columns: auto auto auto;
padding-left: 30px;
padding-right: 30px;
min-height: 500px; }
@media (max-width: 850px) {
.onboardingMessageContainer {
grid-template-columns: none;
grid-template-rows: auto auto auto;
padding-left: 110px;
padding-right: 110px; } }
.onboardingMessage { .onboardingMessage {
height: 340px; height: 340px;
text-align: center; text-align: center;

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

@ -3421,6 +3421,20 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
.submissionStatus .submitStatusTitle { .submissionStatus .submitStatusTitle {
font-size: 20px; } font-size: 20px; }
.onboardingMessageContainer {
display: grid;
grid-column-gap: 21px;
grid-template-columns: auto auto auto;
padding-left: 30px;
padding-right: 30px;
min-height: 500px; }
@media (max-width: 850px) {
.onboardingMessageContainer {
grid-template-columns: none;
grid-template-rows: auto auto auto;
padding-left: 110px;
padding-right: 110px; } }
.onboardingMessage { .onboardingMessage {
height: 340px; height: 340px;
text-align: center; text-align: center;

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

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

@ -1086,23 +1086,3 @@ This reports when a user has seen or clicked a badge/notification in the browser
"event": ["CLICK" | "IMPRESSION"], "event": ["CLICK" | "IMPRESSION"],
} }
``` ```
## Panel interaction pings
This reports when a user opens the panel, views messages and clicks on a message.
For message impressions we concatenate the ids of all messages in the panel.
```
{
"locale": "en-US",
"client_id": "9da773d8-4356-f54f-b7cf-6134726bcf3d",
"version": "70.0a1",
"release_channel": "default",
"addon_version": "20190712095934",
"action": "cfr_user_event",
"source": "CFR",
"message_id": "WHATS_NEW_70",
"event": ["CLICK" | "IMPRESSION"],
"value": { "view": ["application_menu" | "toolbar_dropdown"] }
}
```

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

@ -732,7 +732,6 @@ class _ASRouter {
}); });
ToolbarPanelHub.init(this.waitForInitialized, { ToolbarPanelHub.init(this.waitForInitialized, {
getMessages: this.handleMessageRequest, getMessages: this.handleMessageRequest,
dispatch: this.dispatch,
}); });
this._loadLocalProviders(); this._loadLocalProviders();
@ -900,25 +899,18 @@ class _ASRouter {
let interrupt; let interrupt;
let triplet; let triplet;
// Use control Trailhead Branch (for cards) if we are showing RTAMO.
if (await this._hasAddonAttributionData()) {
return { experiment, interrupt: "control", triplet: "" };
}
// If a value is set in TRAILHEAD_OVERRIDE_PREF, it will be returned and no experiment will be set.
const overrideValue = Services.prefs.getStringPref( const overrideValue = Services.prefs.getStringPref(
TRAILHEAD_CONFIG.OVERRIDE_PREF, TRAILHEAD_CONFIG.OVERRIDE_PREF,
"" ""
); );
if (overrideValue) { if (overrideValue) {
[interrupt, triplet] = overrideValue.split("-"); [interrupt, triplet] = overrideValue.split("-");
}
// Use control Trailhead Branch (for cards) if we are showing RTAMO.
if (await this._hasAddonAttributionData()) {
return {
experiment,
interrupt: "control",
triplet: triplet || "privacy",
};
}
// If a value is set in TRAILHEAD_OVERRIDE_PREF, it will be returned and no experiment will be set.
if (overrideValue) {
return { experiment, interrupt, triplet: triplet || "" }; return { experiment, interrupt, triplet: triplet || "" };
} }
@ -984,7 +976,6 @@ class _ASRouter {
interrupt, interrupt,
triplet, triplet,
} = await this._generateTrailheadBranches(); } = await this._generateTrailheadBranches();
await this.setState({ await this.setState({
trailheadInitialized: true, trailheadInitialized: true,
trailheadInterrupt: interrupt, trailheadInterrupt: interrupt,
@ -1841,6 +1832,11 @@ class _ASRouter {
data: { id: action.data.id }, data: { id: action.data.id },
}); });
break; break;
case "DISMISS_BUNDLE":
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
type: "CLEAR_BUNDLE",
});
break;
case "BLOCK_BUNDLE": case "BLOCK_BUNDLE":
await this.blockMessageById(action.data.bundle.map(b => b.id)); await this.blockMessageById(action.data.bundle.map(b => b.id));
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, { this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
@ -1895,7 +1891,6 @@ class _ASRouter {
break; break;
case "DOORHANGER_TELEMETRY": case "DOORHANGER_TELEMETRY":
case "TOOLBAR_BADGE_TELEMETRY": case "TOOLBAR_BADGE_TELEMETRY":
case "TOOLBAR_PANEL_TELEMETRY":
if (this.dispatchToAS) { if (this.dispatchToAS) {
this.dispatchToAS(ac.ASRouterUserEvent(action.data)); this.dispatchToAS(ac.ASRouterUserEvent(action.data));
} }

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

@ -3,6 +3,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict"; "use strict";
/* globals Localization */ /* globals Localization */
const { FxAccountsConfig } = ChromeUtils.import(
"resource://gre/modules/FxAccountsConfig.jsm"
);
const { AttributionCode } = ChromeUtils.import( const { AttributionCode } = ChromeUtils.import(
"resource:///modules/AttributionCode.jsm" "resource:///modules/AttributionCode.jsm"
); );
@ -18,7 +21,114 @@ const L10N = new Localization([
"browser/newtab/onboarding.ftl", "browser/newtab/onboarding.ftl",
]); ]);
const ONBOARDING_MESSAGES = () => [ const ONBOARDING_MESSAGES = async () => [
{
id: "ONBOARDING_1",
template: "onboarding",
bundled: 3,
order: 2,
content: {
title: { string_id: "onboarding-private-browsing-title" },
text: { string_id: "onboarding-private-browsing-text" },
icon: "privatebrowsing",
primary_button: {
label: { string_id: "onboarding-button-label-try-now" },
action: { type: "OPEN_PRIVATE_BROWSER_WINDOW" },
},
},
trigger: { id: "showOnboarding" },
},
{
id: "ONBOARDING_2",
template: "onboarding",
bundled: 3,
order: 3,
content: {
title: { string_id: "onboarding-screenshots-title" },
text: { string_id: "onboarding-screenshots-text" },
icon: "screenshots",
primary_button: {
label: { string_id: "onboarding-button-label-try-now" },
action: {
type: "OPEN_URL",
data: {
args: "https://screenshots.firefox.com/#tour",
where: "tabshifted",
},
},
},
},
trigger: { id: "showOnboarding" },
},
{
id: "ONBOARDING_3",
template: "onboarding",
bundled: 3,
order: 1,
content: {
title: { string_id: "onboarding-addons-title" },
text: { string_id: "onboarding-addons-text" },
icon: "addons",
primary_button: {
label: { string_id: "onboarding-button-label-try-now" },
action: {
type: "OPEN_ABOUT_PAGE",
data: { args: "addons" },
},
},
},
targeting:
"trailheadInterrupt == 'control' && attributionData.campaign != 'non-fx-button' && attributionData.source != 'addons.mozilla.org'",
trigger: { id: "showOnboarding" },
},
{
id: "ONBOARDING_4",
template: "onboarding",
bundled: 3,
order: 1,
content: {
title: { string_id: "onboarding-ghostery-title" },
text: { string_id: "onboarding-ghostery-text" },
icon: "gift",
primary_button: {
label: { string_id: "onboarding-button-label-try-now" },
action: {
type: "OPEN_URL",
data: {
args: "https://addons.mozilla.org/en-US/firefox/addon/ghostery/",
where: "tabshifted",
},
},
},
},
targeting:
"trailheadInterrupt == 'control' && providerCohorts.onboarding == 'ghostery'",
trigger: { id: "showOnboarding" },
},
{
id: "ONBOARDING_5",
template: "onboarding",
bundled: 3,
order: 4,
content: {
title: { string_id: "onboarding-fxa-title" },
text: { string_id: "onboarding-fxa-text" },
icon: "sync",
primary_button: {
label: { string_id: "onboarding-button-label-get-started" },
action: {
type: "OPEN_URL",
data: {
args: await FxAccountsConfig.promiseEmailFirstURI("onboarding"),
where: "tabshifted",
},
},
},
},
targeting:
"trailheadInterrupt == 'control' && attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
trigger: { id: "showOnboarding" },
},
{ {
id: "TRAILHEAD_1", id: "TRAILHEAD_1",
template: "trailhead", template: "trailhead",
@ -326,13 +436,7 @@ const ONBOARDING_MESSAGES = () => [
{ {
id: "FXA_1", id: "FXA_1",
template: "fxa_overlay", template: "fxa_overlay",
content: {},
trigger: { id: "firstRun" }, trigger: { id: "firstRun" },
includeBundle: {
length: 3,
template: "onboarding",
trigger: { id: "showOnboarding" },
},
}, },
{ {
id: "RETURN_TO_AMO_1", id: "RETURN_TO_AMO_1",
@ -357,11 +461,6 @@ const ONBOARDING_MESSAGES = () => [
label: { string_id: "return-to-amo-get-started-button" }, label: { string_id: "return-to-amo-get-started-button" },
}, },
}, },
includeBundle: {
length: 3,
template: "onboarding",
trigger: { id: "showOnboarding" },
},
targeting: targeting:
"attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'", "attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
trigger: { id: "firstRun" }, trigger: { id: "firstRun" },

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

@ -79,9 +79,9 @@ const MESSAGES = () => [
// Never saw this message or saw it in the past 4 days or more recent // Never saw this message or saw it in the past 4 days or more recent
targeting: `isWhatsNewPanelEnabled && targeting: `isWhatsNewPanelEnabled &&
(earliestFirefoxVersion && firefoxVersion > earliestFirefoxVersion) && (earliestFirefoxVersion && firefoxVersion > earliestFirefoxVersion) &&
(!messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}'] || messageImpressions[.id == 'WHATS_NEW_BADGE_${FIREFOX_VERSION}']|length == 0 ||
(messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}']|length >= 1 && (messageImpressions[.id == 'WHATS_NEW_BADGE_${FIREFOX_VERSION}']|length >= 1 &&
currentDate|date - messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}'][0] <= 4 * 24 * 3600 * 1000))`, currentDate|date - messageImpressions[.id == 'WHATS_NEW_BADGE_${FIREFOX_VERSION}'][0] <= 4 * 24 * 3600 * 1000)`,
}, },
{ {
id: "WHATS_NEW_70_1", id: "WHATS_NEW_70_1",

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

@ -13,11 +13,6 @@ ChromeUtils.defineModuleGetter(
"EveryWindow", "EveryWindow",
"resource:///modules/EveryWindow.jsm" "resource:///modules/EveryWindow.jsm"
); );
ChromeUtils.defineModuleGetter(
this,
"PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm"
);
const WHATSNEW_ENABLED_PREF = "browser.messaging-system.whatsNewPanel.enabled"; const WHATSNEW_ENABLED_PREF = "browser.messaging-system.whatsNewPanel.enabled";
@ -29,16 +24,14 @@ const BUTTON_STRING_ID = "cfr-whatsnew-button";
class _ToolbarPanelHub { class _ToolbarPanelHub {
constructor() { constructor() {
this.triggerId = "whatsNewPanelOpened";
this._showAppmenuButton = this._showAppmenuButton.bind(this); this._showAppmenuButton = this._showAppmenuButton.bind(this);
this._hideAppmenuButton = this._hideAppmenuButton.bind(this); this._hideAppmenuButton = this._hideAppmenuButton.bind(this);
this._showToolbarButton = this._showToolbarButton.bind(this); this._showToolbarButton = this._showToolbarButton.bind(this);
this._hideToolbarButton = this._hideToolbarButton.bind(this); this._hideToolbarButton = this._hideToolbarButton.bind(this);
} }
async init(waitForInitialized, { getMessages, dispatch }) { async init(waitForInitialized, { getMessages }) {
this._getMessages = getMessages; this._getMessages = getMessages;
this._dispatch = dispatch;
// Wait for ASRouter messages to become available in order to know // Wait for ASRouter messages to become available in order to know
// if we can show the What's New panel // if we can show the What's New panel
await waitForInitialized; await waitForInitialized;
@ -139,36 +132,19 @@ class _ToolbarPanelHub {
if (messages && !container.querySelector(".whatsNew-message")) { if (messages && !container.querySelector(".whatsNew-message")) {
let previousDate = 0; let previousDate = 0;
for (let message of messages) { for (let { content } of messages) {
container.appendChild( container.appendChild(
this._createMessageElements(win, doc, message, previousDate) this._createMessageElements(win, doc, content, previousDate)
); );
previousDate = message.content.published_date; previousDate = content.published_date;
} }
} }
// TODO: TELEMETRY
this._onPanelHidden(win); this._onPanelHidden(win);
// Panel impressions are not associated with one particular message
// but with a set of messages. We concatenate message ids and send them
// back for every impression.
const eventId = {
id: messages
.map(({ id }) => id)
.sort()
.join(","),
};
// Check `mainview` attribute to determine if the panel is shown as a
// subview (inside the application menu) or as a toolbar dropdown.
// https://searchfox.org/mozilla-central/rev/07f7390618692fa4f2a674a96b9b677df3a13450/browser/components/customizableui/PanelMultiView.jsm#1268
const mainview = win.PanelUI.whatsNewPanel.hasAttribute("mainview");
this.sendUserEventTelemetry(win, "IMPRESSION", eventId, {
value: { view: mainview ? "toolbar_dropdown" : "application_menu" },
});
} }
_createMessageElements(win, doc, message, previousDate) { _createMessageElements(win, doc, content, previousDate) {
const { content } = message;
const messageEl = this._createElement(doc, "div"); const messageEl = this._createElement(doc, "div");
messageEl.classList.add("whatsNew-message"); messageEl.classList.add("whatsNew-message");
@ -191,7 +167,7 @@ class _ToolbarPanelHub {
csp: null, csp: null,
}); });
this.sendUserEventTelemetry(win, "CLICK", message); // TODO: TELEMETRY
}); });
if (content.icon_url) { if (content.icon_url) {
@ -285,30 +261,6 @@ class _ToolbarPanelHub {
_hideElement(document, id) { _hideElement(document, id) {
document.getElementById(id).setAttribute("hidden", true); document.getElementById(id).setAttribute("hidden", true);
} }
_sendTelemetry(ping) {
this._dispatch({
type: "TOOLBAR_PANEL_TELEMETRY",
data: { action: "cfr_user_event", source: "CFR", ...ping },
});
}
sendUserEventTelemetry(win, event, message, options = {}) {
// Only send pings for non private browsing windows
if (
win &&
!PrivateBrowsingUtils.isBrowserPrivate(
win.ownerGlobal.gBrowser.selectedBrowser
)
) {
this._sendTelemetry({
message_id: message.id,
bucket_id: message.id,
event,
value: options.value,
});
}
}
} }
this._ToolbarPanelHub = _ToolbarPanelHub; this._ToolbarPanelHub = _ToolbarPanelHub;

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

@ -47,7 +47,7 @@ newtab-topsites-save-button = Save
newtab-topsites-preview-button = Preview newtab-topsites-preview-button = Preview
newtab-topsites-add-button = Add newtab-topsites-add-button = Add
## Top Sites - Delete history confirmation dialog. ## Top Sites - Delete history confirmation dialog.
newtab-confirm-delete-history-p1 = Are you sure you want to delete every instance of this page from your history? newtab-confirm-delete-history-p1 = Are you sure you want to delete every instance of this page from your history?
# "This action" refers to deleting a page from history. # "This action" refers to deleting a page from history.
@ -89,7 +89,7 @@ newtab-menu-remove-bookmark = Remove Bookmark
# Bookmark is a verb here. # Bookmark is a verb here.
newtab-menu-bookmark = Bookmark newtab-menu-bookmark = Bookmark
## Context Menu - Downloaded Menu. "Download" in these cases is not a verb, ## Context Menu - Downloaded Menu. "Download" in these cases is not a verb,
## it is a noun. As in, "Copy the link that belongs to this downloaded item". ## it is a noun. As in, "Copy the link that belongs to this downloaded item".
newtab-menu-copy-download-link = Copy Download Link newtab-menu-copy-download-link = Copy Download Link
@ -117,7 +117,7 @@ newtab-label-recommended = Trending
newtab-label-saved = Saved to { -pocket-brand-name } newtab-label-saved = Saved to { -pocket-brand-name }
newtab-label-download = Downloaded newtab-label-download = Downloaded
## Section Menu: These strings are displayed in the section context menu and are ## Section Menu: These strings are displayed in the section context menu and are
## meant as a call to action for the given section. ## meant as a call to action for the given section.
newtab-section-menu-remove-section = Remove Section newtab-section-menu-remove-section = Remove Section
@ -131,13 +131,6 @@ newtab-section-menu-move-up = Move Up
newtab-section-menu-move-down = Move Down newtab-section-menu-move-down = Move Down
newtab-section-menu-privacy-notice = Privacy Notice newtab-section-menu-privacy-notice = Privacy Notice
## Section aria-labels
newtab-section-collapse-section-label =
.aria-label = Collapse Section
newtab-section-expand-section-label =
.aria-label = Expand Section
## Section Headers. ## Section Headers.
newtab-section-header-topsites = Top Sites newtab-section-header-topsites = Top Sites

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

@ -27,5 +27,4 @@ skip-if = (os == "linux") # Test setup only implemented for OSX and Windows
[browser_topsites_section.js] [browser_topsites_section.js]
[browser_asrouter_cfr.js] [browser_asrouter_cfr.js]
skip-if = fission skip-if = fission
skip-if = fission
[browser_asrouter_bookmarkpanel.js] [browser_asrouter_bookmarkpanel.js]

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

@ -83,6 +83,9 @@ add_task(async () => {
".ReturnToAMOContainer", ".ReturnToAMOContainer",
".ReturnToAMOAddonContents", ".ReturnToAMOAddonContents",
".ReturnToAMOIcon", ".ReturnToAMOIcon",
// Regular onboarding cards
".onboardingMessageContainer",
".onboardingMessage",
]) { ]) {
ok(content.document.querySelector(selector), `Should render ${selector}`); ok(content.document.querySelector(selector), `Should render ${selector}`);
} }

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

@ -215,7 +215,6 @@ describe("ASRouter", () => {
Router.waitForInitialized, Router.waitForInitialized,
{ {
getMessages: Router.handleMessageRequest, getMessages: Router.handleMessageRequest,
dispatch: Router.dispatch,
} }
); );
@ -1302,6 +1301,23 @@ describe("ASRouter", () => {
}); });
}); });
describe("#onMessage: DISMISS_BUNDLE", () => {
it("should add all the ids in the bundle to the messageBlockList and send a CLEAR_BUNDLE message", async () => {
await Router.setState({ lastMessageId: "foo" });
const msg = fakeAsyncMessage({
type: "DISMISS_BUNDLE",
data: { bundle: FAKE_BUNDLE },
});
await Router.onMessage(msg);
assert.calledWith(
channel.sendAsyncMessage,
PARENT_TO_CHILD_MESSAGE_NAME,
{ type: "CLEAR_BUNDLE" }
);
});
});
describe("#onMessage: UNBLOCK_MESSAGE_BY_ID", () => { describe("#onMessage: UNBLOCK_MESSAGE_BY_ID", () => {
it("should remove the id from the messageBlockList", async () => { it("should remove the id from the messageBlockList", async () => {
await Router.onMessage( await Router.onMessage(

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

@ -20,6 +20,21 @@ const FAKE_BELOW_SEARCH_SNIPPET = FAKE_LOCAL_MESSAGES.find(
); );
FAKE_MESSAGE = Object.assign({}, FAKE_MESSAGE, { provider: "fakeprovider" }); FAKE_MESSAGE = Object.assign({}, FAKE_MESSAGE, { provider: "fakeprovider" });
const FAKE_BUNDLED_MESSAGE = {
bundle: [
{
id: "foo",
template: "onboarding",
content: {
title: "Foo",
primary_button: { label: "Bar" },
text: "Foo123",
},
},
],
extraTemplateStrings: {},
template: "onboarding",
};
describe("ASRouterUtils", () => { describe("ASRouterUtils", () => {
let global; let global;
@ -63,11 +78,6 @@ describe("ASRouterUISurface", () => {
location: { href: "" }, location: { href: "" },
_listeners: new Set(), _listeners: new Set(),
_visibilityState: "hidden", _visibilityState: "hidden",
head: {
appendChild(el) {
return el;
},
},
get visibilityState() { get visibilityState() {
return this._visibilityState; return this._visibilityState;
}, },
@ -88,15 +98,7 @@ describe("ASRouterUISurface", () => {
return document.createElement("body"); return document.createElement("body");
}, },
getElementById(id) { getElementById(id) {
switch (id) { return id === "header-asrouter-container" ? headerPortal : footerPortal;
case "header-asrouter-container":
return headerPortal;
default:
return footerPortal;
}
},
createElement(tag) {
return document.createElement(tag);
}, },
}; };
global = new GlobalOverrider(); global = new GlobalOverrider();
@ -143,6 +145,11 @@ describe("ASRouterUISurface", () => {
); );
}); });
it("should render the component if a bundle of messages is defined", () => {
wrapper.setState({ bundle: FAKE_BUNDLED_MESSAGE });
assert.isTrue(wrapper.exists());
});
it("should render a preview banner if message provider is preview", () => { it("should render a preview banner if message provider is preview", () => {
wrapper.setState({ message: { ...FAKE_MESSAGE, provider: "preview" } }); wrapper.setState({ message: { ...FAKE_MESSAGE, provider: "preview" } });
assert.isTrue(wrapper.find(".snippets-preview-banner").exists()); assert.isTrue(wrapper.find(".snippets-preview-banner").exists());
@ -166,13 +173,10 @@ describe("ASRouterUISurface", () => {
}); });
it("should render a trailhead message in the header portal", async () => { it("should render a trailhead message in the header portal", async () => {
// wrapper = shallow(<ASRouterUISurface document={fakeDocument} />);
const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find( const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(
msg => msg.template === "trailhead" msg => msg.template === "trailhead"
); );
wrapper.setState({ message }); wrapper.setState({ message });
assert.isTrue(headerPortal.childElementCount > 0); assert.isTrue(headerPortal.childElementCount > 0);
assert.equal(footerPortal.childElementCount, 0); assert.equal(footerPortal.childElementCount, 0);
}); });
@ -366,5 +370,18 @@ describe("ASRouterUISurface", () => {
); );
assert.propertyVal(payload, "source", "NEWTAB_FOOTER_BAR"); assert.propertyVal(payload, "source", "NEWTAB_FOOTER_BAR");
}); });
it("should call .sendTelemetry with the right message data when a bundle is dismissed", () => {
wrapper.instance().dismissBundle([{ id: 1 }, { id: 2 }, { id: 3 }])();
assert.calledOnce(ASRouterUtils.sendTelemetry);
assert.calledWith(ASRouterUtils.sendTelemetry, {
action: "onboarding_user_event",
event: "DISMISS",
id: "onboarding-cards",
message_id: "1,2,3",
source: "onboarding-cards",
});
});
}); });
}); });

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

@ -4,14 +4,12 @@ export const PARENT_TO_CHILD_MESSAGE_NAME = "ASRouter:parent-to-child";
export const FAKE_LOCAL_MESSAGES = [ export const FAKE_LOCAL_MESSAGES = [
{ {
id: "foo", id: "foo",
provider: "snippets",
template: "simple_snippet", template: "simple_snippet",
content: { title: "Foo", body: "Foo123" }, content: { title: "Foo", body: "Foo123" },
}, },
{ {
id: "foo1", id: "foo1",
template: "simple_snippet", template: "simple_snippet",
provider: "snippets",
bundled: 2, bundled: 2,
order: 1, order: 1,
content: { title: "Foo1", body: "Foo123-1" }, content: { title: "Foo1", body: "Foo123-1" },
@ -19,7 +17,6 @@ export const FAKE_LOCAL_MESSAGES = [
{ {
id: "foo2", id: "foo2",
template: "simple_snippet", template: "simple_snippet",
provider: "snippets",
bundled: 2, bundled: 2,
order: 2, order: 2,
content: { title: "Foo2", body: "Foo123-2" }, content: { title: "Foo2", body: "Foo123-2" },
@ -32,19 +29,16 @@ export const FAKE_LOCAL_MESSAGES = [
{ id: "baz", content: { title: "Foo", body: "Foo123" } }, { id: "baz", content: { title: "Foo", body: "Foo123" } },
{ {
id: "newsletter", id: "newsletter",
provider: "snippets",
template: "newsletter_snippet", template: "newsletter_snippet",
content: { title: "Foo", body: "Foo123" }, content: { title: "Foo", body: "Foo123" },
}, },
{ {
id: "fxa", id: "fxa",
provider: "snippets",
template: "fxa_signup_snippet", template: "fxa_signup_snippet",
content: { title: "Foo", body: "Foo123" }, content: { title: "Foo", body: "Foo123" },
}, },
{ {
id: "belowsearch", id: "belowsearch",
provider: "snippets",
template: "simple_below_search_snippet", template: "simple_below_search_snippet",
content: { text: "Foo" }, content: { text: "Foo" },
}, },

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

@ -1,210 +0,0 @@
import {
helpers,
FirstRun,
FLUENT_FILES,
} from "content-src/asrouter/templates/FirstRun/FirstRun";
import { Interrupt } from "content-src/asrouter/templates/FirstRun/Interrupt";
import { Triplets } from "content-src/asrouter/templates/FirstRun/Triplets";
import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
import { mount } from "enzyme";
import React from "react";
const FAKE_TRIPLETS = [
{
id: "CARD_1",
content: {
title: { string_id: "onboarding-private-browsing-title" },
text: { string_id: "onboarding-private-browsing-text" },
icon: "icon",
primary_button: {
label: { string_id: "onboarding-button-label-try-now" },
action: {
type: "OPEN_URL",
data: { args: "https://example.com/" },
},
},
},
},
];
const FAKE_FLOW_PARAMS = {
deviceId: "foo",
flowId: "abc1",
flowBeginTime: 1234,
};
async function getTestMessage(id) {
const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(
msg => msg.id === id
);
return { ...message, bundle: FAKE_TRIPLETS };
}
describe("<FirstRun>", () => {
let wrapper;
let message;
let fakeDoc;
let sandbox;
async function setup() {
sandbox = sinon.createSandbox();
message = await getTestMessage("TRAILHEAD_1");
fakeDoc = {
body: document.createElement("body"),
head: document.createElement("head"),
createElement: type => document.createElement(type),
getElementById: () => document.createElement("div"),
activeElement: document.createElement("div"),
};
sandbox
.stub(global, "fetch")
.withArgs("http://fake.com/endpoint")
.resolves({
ok: true,
status: 200,
json: () => Promise.resolve(FAKE_FLOW_PARAMS),
});
wrapper = mount(
<FirstRun
message={message}
document={fakeDoc}
dispatch={() => {}}
sendUserActionTelemetry={() => {}}
/>
);
}
beforeEach(setup);
afterEach(() => {
sandbox.restore();
});
it("should render", () => {
assert.ok(wrapper);
});
describe("with both interrupt and triplets", () => {
it("should render interrupt and triplets", () => {
assert.lengthOf(wrapper.find(Interrupt), 1, "<Interrupt>");
assert.lengthOf(wrapper.find(Triplets), 1, "<Triplets>");
});
it("should show the card panel and hide the content on the Triplets", () => {
// This is so the container shows up in the background but we can fade in the content when intterupt is closed.
const tripletsProps = wrapper.find(Triplets).props();
assert.propertyVal(tripletsProps, "showCardPanel", true);
assert.propertyVal(tripletsProps, "showContent", false);
});
it("should set the UTM term to trailhead-join (for the traihead-join message)", () => {
const iProps = wrapper.find(Interrupt).props();
const tProps = wrapper.find(Triplets).props();
assert.propertyVal(iProps, "UTMTerm", "trailhead-join");
assert.propertyVal(tProps, "UTMTerm", "trailhead-join-card");
});
});
describe("with an interrupt but no triplets", () => {
beforeEach(() => {
message.bundle = []; // Empty triplets
wrapper = mount(<FirstRun message={message} document={fakeDoc} />);
});
it("should render interrupt but no triplets", () => {
assert.lengthOf(wrapper.find(Interrupt), 1, "<Interrupt>");
assert.lengthOf(wrapper.find(Triplets), 0, "<Triplets>");
});
});
describe("with triplets but no interrupt", () => {
it("should render interrupt but no triplets", () => {
delete message.content; // Empty interrupt
wrapper = mount(<FirstRun message={message} document={fakeDoc} />);
assert.lengthOf(wrapper.find(Interrupt), 0, "<Interrupt>");
assert.lengthOf(wrapper.find(Triplets), 1, "<Triplets>");
});
});
describe("with no triplets or interrupt", () => {
it("should render empty", () => {
message = { type: "FOO_123" };
wrapper = mount(<FirstRun message={message} document={fakeDoc} />);
assert.isTrue(wrapper.isEmptyRender());
});
});
it("should load flow params on mount if fxaEndpoint is defined", () => {
const spy = sandbox.spy(helpers, "fetchFlowParams");
wrapper = mount(
<FirstRun
message={message}
document={fakeDoc}
dispatch={() => {}}
fxaEndpoint="https://foo.com"
/>
);
assert.calledOnce(spy);
});
it("should load flow params onUpdate if fxaEndpoint is not defined on mount and then later defined", () => {
const spy = sandbox.spy(helpers, "fetchFlowParams");
wrapper = mount(
<FirstRun message={message} document={fakeDoc} dispatch={() => {}} />
);
assert.notCalled(spy);
wrapper.setProps({ fxaEndpoint: "https://foo.com" });
assert.calledOnce(spy);
});
it("should not load flow params again onUpdate if they were already set", () => {
const spy = sandbox.spy(helpers, "fetchFlowParams");
wrapper = mount(
<FirstRun
message={message}
document={fakeDoc}
dispatch={() => {}}
fxaEndpoint="https://foo.com"
/>
);
wrapper.setProps({ foo: "bar" });
wrapper.setProps({ foo: "baz" });
assert.calledOnce(spy);
});
it("should load fluent files on mount", () => {
assert.lengthOf(fakeDoc.head.querySelectorAll("link"), FLUENT_FILES.length);
});
it("should hide the interrupt and show the triplets when onNextScene is called", () => {
// Simulate calling next scene
wrapper
.find(Interrupt)
.find(".trailheadStart")
.simulate("click");
assert.lengthOf(wrapper.find(Interrupt), 0, "Interrupt hidden");
assert.isTrue(
wrapper
.find(Triplets)
.find(".trailheadCardGrid")
.hasClass("show"),
"Show triplet content"
);
});
it("should hide triplets when closeTriplets is called", () => {
// Simulate calling next scene
wrapper
.find(Triplets)
.find(".icon-dismiss")
.simulate("click");
assert.isFalse(
wrapper
.find(Triplets)
.find(".trailheadCardGrid")
.hasClass("show"),
"Show triplet content"
);
});
});

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

@ -1,37 +0,0 @@
import { Interrupt } from "content-src/asrouter/templates/FirstRun/Interrupt";
import { ReturnToAMO } from "content-src/asrouter/templates/ReturnToAMO/ReturnToAMO";
import { StartupOverlay } from "content-src/asrouter/templates/StartupOverlay/StartupOverlay";
import { Trailhead } from "content-src/asrouter/templates//Trailhead/Trailhead";
import { shallow } from "enzyme";
import React from "react";
describe("<Interrupt>", () => {
let wrapper;
it("should render Return TO AMO when the message has a template of return_to_amo_overlay", () => {
wrapper = shallow(
<Interrupt
message={{ id: "FOO", content: {}, template: "return_to_amo_overlay" }}
/>
);
assert.lengthOf(wrapper.find(ReturnToAMO), 1);
});
it("should render Trailhead when the message has a template of trailhead", () => {
wrapper = shallow(
<Interrupt message={{ id: "FOO", content: {}, template: "trailhead" }} />
);
assert.lengthOf(wrapper.find(Trailhead), 1);
});
it("should render StartupOverlay when the message has a template of fxa_overlay", () => {
wrapper = shallow(
<Interrupt message={{ id: "FOO", template: "fxa_overlay" }} />
);
assert.lengthOf(wrapper.find(StartupOverlay), 1);
});
it("should throw an error if another type of message is dispatched", () => {
assert.throws(() => {
wrapper = shallow(
<Interrupt message={{ id: "FOO", template: "something" }} />
);
});
});
});

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

@ -4,7 +4,7 @@ import { OnboardingMessageProvider } from "lib/OnboardingMessageProvider.jsm";
import React from "react"; import React from "react";
import { Trailhead } from "content-src/asrouter/templates/Trailhead/Trailhead"; import { Trailhead } from "content-src/asrouter/templates/Trailhead/Trailhead";
export const CARDS = [ const CARDS = [
{ {
content: { content: {
title: { string_id: "onboarding-private-browsing-title" }, title: { string_id: "onboarding-private-browsing-title" },
@ -27,13 +27,11 @@ describe("<Trailhead>", () => {
let dispatch; let dispatch;
let onAction; let onAction;
let sandbox; let sandbox;
let onNextScene;
beforeEach(async () => { beforeEach(async () => {
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
dispatch = sandbox.stub(); dispatch = sandbox.stub();
onAction = sandbox.stub(); onAction = sandbox.stub();
onNextScene = sandbox.stub();
sandbox.stub(global, "fetch").resolves({ sandbox.stub(global, "fetch").resolves({
ok: true, ok: true,
status: 200, status: 200,
@ -61,12 +59,10 @@ describe("<Trailhead>", () => {
wrapper = mount( wrapper = mount(
<Trailhead <Trailhead
message={message} message={message}
UTMTerm={message.utm_term}
fxaEndpoint="https://accounts.firefox.com/endpoint" fxaEndpoint="https://accounts.firefox.com/endpoint"
dispatch={dispatch} dispatch={dispatch}
onAction={onAction} onAction={onAction}
document={fakeDocument} document={fakeDocument}
onNextScene={onNextScene}
/> />
); );
}); });
@ -75,13 +71,11 @@ describe("<Trailhead>", () => {
sandbox.restore(); sandbox.restore();
}); });
it("should emit UserEvent SKIPPED_SIGNIN and call nextScene when you click the start browsing button", () => { it("should emit UserEvent SKIPPED_SIGNIN when you click the start browsing button", () => {
let skipButton = wrapper.find(".trailheadStart"); let skipButton = wrapper.find(".trailheadStart");
assert.ok(skipButton.exists()); assert.ok(skipButton.exists());
skipButton.simulate("click"); skipButton.simulate("click");
assert.calledOnce(onNextScene);
assert.calledOnce(dispatch); assert.calledOnce(dispatch);
assert.isUserEventAction(dispatch.firstCall.args[0]); assert.isUserEventAction(dispatch.firstCall.args[0]);
assert.calledWith( assert.calledWith(
@ -125,6 +119,37 @@ describe("<Trailhead>", () => {
); );
}); });
it("should add utm_* query params to card actions", () => {
let { action } = CARDS[0].content.primary_button;
wrapper.instance().onCardAction(action);
assert.calledOnce(onAction);
const url = onAction.firstCall.args[0].data.args;
assert.equal(
url,
"https://example.com/?utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-join-card"
);
});
it("should add flow parameters to card action urls if addFlowParams is true", () => {
let action = {
type: "OPEN_URL",
addFlowParams: true,
data: { args: "https://example.com/path?foo=bar" },
};
wrapper.setState({
deviceId: "abc",
flowId: "123",
flowBeginTime: 456,
});
wrapper.instance().onCardAction(action);
assert.calledOnce(onAction);
const url = onAction.firstCall.args[0].data.args;
assert.equal(
url,
"https://example.com/path?foo=bar&utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-join-card&device_id=abc&flow_id=123&flow_begin_time=456"
);
});
it("should keep focus in dialog when blurring start button", () => { it("should keep focus in dialog when blurring start button", () => {
const skipButton = wrapper.find(".trailheadStart"); const skipButton = wrapper.find(".trailheadStart");
sandbox.stub(dummyNode, "focus"); sandbox.stub(dummyNode, "focus");

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

@ -1,96 +0,0 @@
import { mount } from "enzyme";
import { Triplets } from "content-src/asrouter/templates/FirstRun/Triplets";
import { OnboardingCard } from "content-src/asrouter/templates/OnboardingMessage/OnboardingMessage";
import React from "react";
const CARDS = [
{
id: "CARD_1",
content: {
title: { string_id: "onboarding-private-browsing-title" },
text: { string_id: "onboarding-private-browsing-text" },
icon: "icon",
primary_button: {
label: { string_id: "onboarding-button-label-try-now" },
action: {
type: "OPEN_URL",
data: { args: "https://example.com/" },
},
},
},
},
];
describe("<Triplets>", () => {
let wrapper;
let sandbox;
let sendTelemetryStub;
let onAction;
let onHide;
async function setup() {
sandbox = sinon.createSandbox();
sendTelemetryStub = sandbox.stub();
onAction = sandbox.stub();
onHide = sandbox.stub();
wrapper = mount(
<Triplets
cards={CARDS}
showCardPanel={true}
showContent={true}
hideContainer={onHide}
onAction={onAction}
UTMTerm="trailhead-join-card"
sendUserActionTelemetry={sendTelemetryStub}
/>
);
}
beforeEach(setup);
afterEach(() => {
sandbox.restore();
});
it("should add an expanded class to container if props.showCardPanel is true", () => {
wrapper.setProps({ showCardPanel: true });
assert.isTrue(
wrapper.find(".trailheadCards").hasClass("expanded"),
"has .expanded)"
);
});
it("should add a collapsed class to container if props.showCardPanel is true", () => {
wrapper.setProps({ showCardPanel: false });
assert.isFalse(
wrapper.find(".trailheadCards").hasClass("expanded"),
"has .expanded)"
);
});
it("should send telemetry and call props.hideContainer when the dismiss button is clicked", () => {
wrapper.find("button.icon-dismiss").simulate("click");
assert.calledOnce(onHide);
assert.calledWith(sendTelemetryStub, {
event: "DISMISS",
message_id: CARDS[0].id,
id: "onboarding-cards",
action: "onboarding_user_event",
});
});
it("should add utm_* query params to card actions and send the right ping when a card button is clicked", () => {
wrapper
.find(OnboardingCard)
.find("button.onboardingButton")
.simulate("click");
assert.calledOnce(onAction);
const url = onAction.firstCall.args[0].data.args;
assert.equal(
url,
"https://example.com/?utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-join-card"
);
assert.calledWith(sendTelemetryStub, {
event: "CLICK_BUTTON",
message_id: CARDS[0].id,
id: "TRAILHEAD",
});
});
});

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

@ -69,6 +69,21 @@ describe("CollapsibleSection", () => {
.simulate("click"); .simulate("click");
}); });
it("should fire a pref change event when section title arrow is clicked", done => {
function dispatch(a) {
if (a.type === at.UPDATE_SECTION_PREFS) {
assert.equal(a.data.id, DEFAULT_PROPS.id);
assert.equal(a.data.value.collapsed, true);
done();
}
}
setup({ dispatch });
wrapper
.find(".click-target")
.at(1)
.simulate("click");
});
it("should not fire a pref change when section title is clicked if sectionBody is falsy", () => { it("should not fire a pref change when section title is clicked if sectionBody is falsy", () => {
const dispatch = sinon.spy(); const dispatch = sinon.spy();
setup({ dispatch }); setup({ dispatch });

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

@ -128,10 +128,7 @@ describe("<ContextMenu>", () => {
it("should be tabbable", () => { it("should be tabbable", () => {
const options = [{ label: "item1", icon: "icon1" }, { type: "separator" }]; const options = [{ label: "item1", icon: "icon1" }, { type: "separator" }];
const wrapper = mount(<ContextMenu {...DEFAULT_PROPS} options={options} />); const wrapper = mount(<ContextMenu {...DEFAULT_PROPS} options={options} />);
assert.equal( assert.equal(wrapper.find(".context-menu-item").props().role, "menuitem");
wrapper.find(".context-menu-item").props().role,
"presentation"
);
}); });
it("should call onUpdate with false when an option is clicked", () => { it("should call onUpdate with false when an option is clicked", () => {
const onUpdate = sinon.spy(); const onUpdate = sinon.spy();

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

@ -66,6 +66,10 @@ describe("<ReturnToAMO>", () => {
sendUserActionTelemetryStub.reset(); sendUserActionTelemetryStub.reset();
}); });
it("should call onReady on componentDidMount", () => {
assert.calledOnce(onReady);
});
it("should send telemetry on block", () => { it("should send telemetry on block", () => {
wrapper.instance().onBlockButton(); wrapper.instance().onBlockButton();

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

@ -1,40 +1,44 @@
import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm"; import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
import { mount } from "enzyme"; import { mount } from "enzyme";
import React from "react"; import React from "react";
import { StartupOverlay } from "content-src/asrouter/templates/StartupOverlay/StartupOverlay"; import { _StartupOverlay as StartupOverlay } from "content-src/asrouter/templates/StartupOverlay/StartupOverlay";
describe("<StartupOverlay>", () => { describe("<StartupOverlay>", () => {
let wrapper; let wrapper;
let dispatch; let dispatch;
let onReady;
let onBlock; let onBlock;
let sandbox; let sandbox;
beforeEach(() => { beforeEach(() => {
sandbox = sinon.createSandbox(); sandbox = sinon.sandbox.create();
dispatch = sandbox.stub(); dispatch = sandbox.stub();
onReady = sandbox.stub();
onBlock = sandbox.stub(); onBlock = sandbox.stub();
wrapper = mount(<StartupOverlay onBlock={onBlock} dispatch={dispatch} />); wrapper = mount(
<StartupOverlay onBlock={onBlock} onReady={onReady} dispatch={dispatch} />
);
}); });
afterEach(() => { afterEach(() => {
sandbox.restore(); sandbox.restore();
}); });
it("should add show class after mount and timeout", async () => { it("should not render if state.show is false", () => {
wrapper.setState({ overlayRemoved: true });
assert.isTrue(wrapper.isEmptyRender());
});
it("should call prop.onReady after mount + timeout", async () => {
const clock = sandbox.useFakeTimers(); const clock = sandbox.useFakeTimers();
wrapper = mount(<StartupOverlay onBlock={onBlock} dispatch={dispatch} />); wrapper = mount(
assert.isFalse( <StartupOverlay onBlock={onBlock} onReady={onReady} dispatch={dispatch} />
wrapper.find(".overlay-wrapper").hasClass("show"),
".overlay-wrapper does not have .show class"
); );
wrapper.setState({ overlayRemoved: false });
clock.tick(10); clock.tick(10);
wrapper.update();
assert.isTrue( assert.calledOnce(onReady);
wrapper.find(".overlay-wrapper").hasClass("show"),
".overlay-wrapper has .show class"
);
}); });
it("should emit UserEvent SKIPPED_SIGNIN when you click the skip button", () => { it("should emit UserEvent SKIPPED_SIGNIN when you click the skip button", () => {

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

@ -1,18 +0,0 @@
import { addUtmParams } from "content-src/asrouter/templates/FirstRun/addUtmParams";
describe("addUtmParams", () => {
it("should convert a string URL", () => {
const result = addUtmParams("https://foo.com", "foo");
assert.equal(result.hostname, "foo.com");
});
it("should add all base params", () => {
assert.match(
addUtmParams(new URL("https://foo.com"), "foo").toString(),
/utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral/
);
});
it("should add utm_term", () => {
const params = addUtmParams(new URL("https://foo.com"), "foo").searchParams;
assert.equal(params.get("utm_term"), "foo", "utm_term");
});
});

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

@ -16,8 +16,6 @@ describe("ToolbarPanelHub", () => {
let removeObserverStub; let removeObserverStub;
let getBoolPrefStub; let getBoolPrefStub;
let waitForInitializedStub; let waitForInitializedStub;
let isBrowserPrivateStub;
let fakeDispatch;
beforeEach(async () => { beforeEach(async () => {
sandbox = sinon.createSandbox(); sandbox = sinon.createSandbox();
@ -30,7 +28,6 @@ describe("ToolbarPanelHub", () => {
querySelector: sandbox.stub().returns(null), querySelector: sandbox.stub().returns(null),
appendChild: sandbox.stub(), appendChild: sandbox.stub(),
addEventListener: sandbox.stub(), addEventListener: sandbox.stub(),
hasAttribute: sandbox.stub(),
}; };
fakeDocument = { fakeDocument = {
l10n: { l10n: {
@ -61,10 +58,6 @@ describe("ToolbarPanelHub", () => {
MozXULElement: { insertFTLIfNeeded: sandbox.stub() }, MozXULElement: { insertFTLIfNeeded: sandbox.stub() },
ownerGlobal: { ownerGlobal: {
openLinkIn: sandbox.stub(), openLinkIn: sandbox.stub(),
gBrowser: "gBrowser",
},
PanelUI: {
whatsNewPanel: fakeElementById,
}, },
}; };
everyWindowStub = { everyWindowStub = {
@ -74,8 +67,6 @@ describe("ToolbarPanelHub", () => {
addObserverStub = sandbox.stub(); addObserverStub = sandbox.stub();
removeObserverStub = sandbox.stub(); removeObserverStub = sandbox.stub();
getBoolPrefStub = sandbox.stub(); getBoolPrefStub = sandbox.stub();
fakeDispatch = sandbox.stub();
isBrowserPrivateStub = sandbox.stub();
globals.set("EveryWindow", everyWindowStub); globals.set("EveryWindow", everyWindowStub);
globals.set("Services", { globals.set("Services", {
...Services, ...Services,
@ -85,9 +76,6 @@ describe("ToolbarPanelHub", () => {
getBoolPref: getBoolPrefStub, getBoolPref: getBoolPrefStub,
}, },
}); });
globals.set("PrivateBrowsingUtils", {
isBrowserPrivate: isBrowserPrivateStub,
});
}); });
afterEach(() => { afterEach(() => {
instance.uninit(); instance.uninit();
@ -97,10 +85,10 @@ describe("ToolbarPanelHub", () => {
it("should create an instance", () => { it("should create an instance", () => {
assert.ok(instance); assert.ok(instance);
}); });
it("should not enableAppmenuButton() on init() if pref is not enabled", async () => { it("should not enableAppmenuButton() on init() if pref is not enabled", () => {
getBoolPrefStub.returns(false); getBoolPrefStub.returns(false);
instance.enableAppmenuButton = sandbox.stub(); instance.enableAppmenuButton = sandbox.stub();
await instance.init(waitForInitializedStub, { getMessages: () => {} }); instance.init(waitForInitializedStub, { getMessages: () => {} });
assert.notCalled(instance.enableAppmenuButton); assert.notCalled(instance.enableAppmenuButton);
}); });
it("should enableAppmenuButton() on init() if pref is enabled", async () => { it("should enableAppmenuButton() on init() if pref is enabled", async () => {
@ -227,221 +215,85 @@ describe("ToolbarPanelHub", () => {
instance._hideToolbarButton(fakeWindow); instance._hideToolbarButton(fakeWindow);
assert.calledWith(fakeElementById.setAttribute, "hidden", true); assert.calledWith(fakeElementById.setAttribute, "hidden", true);
}); });
describe("#renderMessages", () => { it("should render messages to the panel on renderMessages()", async () => {
let getMessagesStub; const messages = (await PanelTestProvider.getMessages()).filter(
beforeEach(() => { m => m.template === "whatsnew_panel_message"
getMessagesStub = sandbox.stub(); );
instance.init(waitForInitializedStub, { messages[0].content.link_text = { string_id: "link_text_id" };
getMessages: getMessagesStub, instance.init(waitForInitializedStub, {
dispatch: fakeDispatch, getMessages: sandbox
}); .stub()
.returns([messages[0], messages[2], messages[1]]),
}); });
it("should render messages to the panel on renderMessages()", async () => { await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
const messages = (await PanelTestProvider.getMessages()).filter( for (let message of messages) {
m => m.template === "whatsnew_panel_message" assert.ok(
createdElements.find(
el => el.tagName === "h2" && el.textContent === message.content.title
)
); );
messages[0].content.link_text = { string_id: "link_text_id" }; assert.ok(
createdElements.find(
el => el.tagName === "p" && el.textContent === message.content.body
)
);
}
// Call the click handler to make coverage happy.
eventListeners.click();
assert.calledOnce(fakeWindow.ownerGlobal.openLinkIn);
});
it("should only render unique dates (no duplicates)", async () => {
instance._createDateElement = sandbox.stub();
const messages = (await PanelTestProvider.getMessages()).filter(
m => m.template === "whatsnew_panel_message"
);
const uniqueDates = [
...new Set(messages.map(m => m.content.published_date)),
];
instance.init(waitForInitializedStub, {
getMessages: sandbox.stub().returns(messages),
});
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
assert.callCount(instance._createDateElement, uniqueDates.length);
});
it("should listen for panelhidden and remove the toolbar button", async () => {
instance.init(waitForInitializedStub, {
getMessages: sandbox.stub().returns([]),
});
fakeDocument.getElementById
.withArgs("customizationui-widget-panel")
.returns(null);
getMessagesStub.returns([messages[0], messages[2], messages[1]]); await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); assert.notCalled(fakeElementById.addEventListener);
});
it("should listen for panelhidden and remove the toolbar button", async () => {
instance.init(waitForInitializedStub, {
getMessages: sandbox.stub().returns([]),
});
for (let message of messages) { await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
assert.ok(
createdElements.find( assert.calledOnce(fakeElementById.addEventListener);
el => assert.calledWithExactly(
el.tagName === "h2" && el.textContent === message.content.title fakeElementById.addEventListener,
) "popuphidden",
); sinon.match.func,
assert.ok( {
createdElements.find( once: true,
el => el.tagName === "p" && el.textContent === message.content.body
)
);
} }
// Call the click handler to make coverage happy. );
eventListeners.click(); const [, cb] = fakeElementById.addEventListener.firstCall.args;
assert.calledOnce(fakeWindow.ownerGlobal.openLinkIn);
});
it("should only render unique dates (no duplicates)", async () => {
instance._createDateElement = sandbox.stub();
const messages = (await PanelTestProvider.getMessages()).filter(
m => m.template === "whatsnew_panel_message"
);
const uniqueDates = [
...new Set(messages.map(m => m.content.published_date)),
];
getMessagesStub.returns(messages);
await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); assert.notCalled(everyWindowStub.unregisterCallback);
assert.callCount(instance._createDateElement, uniqueDates.length); cb();
});
it("should listen for panelhidden and remove the toolbar button", async () => {
getMessagesStub.returns([]);
fakeDocument.getElementById
.withArgs("customizationui-widget-panel")
.returns(null);
await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); assert.calledOnce(everyWindowStub.unregisterCallback);
assert.calledWithExactly(
assert.notCalled(fakeElementById.addEventListener); everyWindowStub.unregisterCallback,
}); "whats-new-menu-button"
it("should listen for panelhidden and remove the toolbar button", async () => { );
getMessagesStub.returns([]);
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
assert.calledOnce(fakeElementById.addEventListener);
assert.calledWithExactly(
fakeElementById.addEventListener,
"popuphidden",
sinon.match.func,
{
once: true,
}
);
const [, cb] = fakeElementById.addEventListener.firstCall.args;
assert.notCalled(everyWindowStub.unregisterCallback);
cb();
assert.calledOnce(everyWindowStub.unregisterCallback);
assert.calledWithExactly(
everyWindowStub.unregisterCallback,
"whats-new-menu-button"
);
});
describe("#IMPRESSION", () => {
it("should dispatch a IMPRESSION for messages", async () => {
// means panel is triggered from the toolbar button
fakeElementById.hasAttribute.returns(true);
const messages = (await PanelTestProvider.getMessages()).filter(
m => m.template === "whatsnew_panel_message"
);
getMessagesStub.returns(messages);
const spy = sandbox.spy(instance, "sendUserEventTelemetry");
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
assert.calledOnce(spy);
assert.calledOnce(fakeDispatch);
assert.propertyVal(
spy.firstCall.args[2],
"id",
messages
.map(({ id }) => id)
.sort()
.join(",")
);
});
it("should dispatch a CLICK for clicking a message", async () => {
// means panel is triggered from the toolbar button
fakeElementById.hasAttribute.returns(true);
// Force to render the message
fakeElementById.querySelector.returns(null);
const messages = (await PanelTestProvider.getMessages()).filter(
m => m.template === "whatsnew_panel_message"
);
getMessagesStub.returns([messages[0]]);
const spy = sandbox.spy(instance, "sendUserEventTelemetry");
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
assert.calledOnce(spy);
assert.calledOnce(fakeDispatch);
spy.resetHistory();
// Message click event listener cb
eventListeners.click();
assert.calledOnce(spy);
assert.calledWithExactly(spy, fakeWindow, "CLICK", messages[0]);
});
it("should dispatch a IMPRESSION with toolbar_dropdown", async () => {
// means panel is triggered from the toolbar button
fakeElementById.hasAttribute.returns(true);
const messages = (await PanelTestProvider.getMessages()).filter(
m => m.template === "whatsnew_panel_message"
);
getMessagesStub.resolves(messages);
const spy = sandbox.spy(instance, "sendUserEventTelemetry");
const panelPingId = messages
.map(({ id }) => id)
.sort()
.join(",");
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
assert.calledOnce(spy);
assert.calledWithExactly(
spy,
fakeWindow,
"IMPRESSION",
{
id: panelPingId,
},
{
value: {
view: "toolbar_dropdown",
},
}
);
assert.calledOnce(fakeDispatch);
const {
args: [dispatchPayload],
} = fakeDispatch.lastCall;
assert.propertyVal(dispatchPayload, "type", "TOOLBAR_PANEL_TELEMETRY");
assert.propertyVal(dispatchPayload.data, "message_id", panelPingId);
assert.propertyVal(
dispatchPayload.data.value,
"view",
"toolbar_dropdown"
);
});
it("should dispatch a IMPRESSION with application_menu", async () => {
// means panel is triggered as a subview in the application menu
fakeElementById.hasAttribute.returns(false);
const messages = (await PanelTestProvider.getMessages()).filter(
m => m.template === "whatsnew_panel_message"
);
getMessagesStub.resolves(messages);
const spy = sandbox.spy(instance, "sendUserEventTelemetry");
const panelPingId = messages
.map(({ id }) => id)
.sort()
.join(",");
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
assert.calledOnce(spy);
assert.calledWithExactly(
spy,
fakeWindow,
"IMPRESSION",
{
id: panelPingId,
},
{
value: {
view: "application_menu",
},
}
);
assert.calledOnce(fakeDispatch);
const {
args: [dispatchPayload],
} = fakeDispatch.lastCall;
assert.propertyVal(dispatchPayload, "type", "TOOLBAR_PANEL_TELEMETRY");
assert.propertyVal(dispatchPayload.data, "message_id", panelPingId);
assert.propertyVal(
dispatchPayload.data.value,
"view",
"application_menu"
);
});
});
}); });
}); });

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

@ -47,7 +47,7 @@ newtab-topsites-save-button = Save
newtab-topsites-preview-button = Preview newtab-topsites-preview-button = Preview
newtab-topsites-add-button = Add newtab-topsites-add-button = Add
## Top Sites - Delete history confirmation dialog. ## Top Sites - Delete history confirmation dialog.
newtab-confirm-delete-history-p1 = Are you sure you want to delete every instance of this page from your history? newtab-confirm-delete-history-p1 = Are you sure you want to delete every instance of this page from your history?
# "This action" refers to deleting a page from history. # "This action" refers to deleting a page from history.
@ -89,7 +89,7 @@ newtab-menu-remove-bookmark = Remove Bookmark
# Bookmark is a verb here. # Bookmark is a verb here.
newtab-menu-bookmark = Bookmark newtab-menu-bookmark = Bookmark
## Context Menu - Downloaded Menu. "Download" in these cases is not a verb, ## Context Menu - Downloaded Menu. "Download" in these cases is not a verb,
## it is a noun. As in, "Copy the link that belongs to this downloaded item". ## it is a noun. As in, "Copy the link that belongs to this downloaded item".
newtab-menu-copy-download-link = Copy Download Link newtab-menu-copy-download-link = Copy Download Link
@ -117,7 +117,7 @@ newtab-label-recommended = Trending
newtab-label-saved = Saved to { -pocket-brand-name } newtab-label-saved = Saved to { -pocket-brand-name }
newtab-label-download = Downloaded newtab-label-download = Downloaded
## Section Menu: These strings are displayed in the section context menu and are ## Section Menu: These strings are displayed in the section context menu and are
## meant as a call to action for the given section. ## meant as a call to action for the given section.
newtab-section-menu-remove-section = Remove Section newtab-section-menu-remove-section = Remove Section
@ -131,13 +131,6 @@ newtab-section-menu-move-up = Move Up
newtab-section-menu-move-down = Move Down newtab-section-menu-move-down = Move Down
newtab-section-menu-privacy-notice = Privacy Notice newtab-section-menu-privacy-notice = Privacy Notice
## Section aria-labels
newtab-section-collapse-section-label =
.aria-label = Collapse Section
newtab-section-expand-section-label =
.aria-label = Expand Section
## Section Headers. ## Section Headers.
newtab-section-header-topsites = Top Sites newtab-section-header-topsites = Top Sites