зеркало из https://github.com/mozilla/gecko-dev.git
Backed out changeset 2cb4cc20ea9d (bug 1569306) for gecko decision bustage CLOSED TREE
This commit is contained in:
Родитель
97c5d52826
Коммит
dfedc5dd8a
|
@ -44,7 +44,6 @@ module.exports = {
|
|||
// These files use fluent-dom to insert content
|
||||
"files": [
|
||||
"content-src/asrouter/templates/OnboardingMessage/**",
|
||||
"content-src/asrouter/templates/FirstRun/**",
|
||||
"content-src/asrouter/templates/Trailhead/**",
|
||||
"content-src/asrouter/templates/StartupOverlay/StartupOverlay.jsx",
|
||||
"content-src/components/TopSites/**",
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
# 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 { LocalizationProvider } from "fluent-react";
|
||||
import { NEWTAB_DARK_THEME } from "content-src/lib/constants";
|
||||
import { OnboardingMessage } from "./templates/OnboardingMessage/OnboardingMessage";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { ReturnToAMO } from "./templates/ReturnToAMO/ReturnToAMO";
|
||||
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 OUTGOING_MESSAGE_NAME = "ASRouter:child-to-parent";
|
||||
const TEMPLATES_ABOVE_PAGE = [
|
||||
"trailhead",
|
||||
"fxa_overlay",
|
||||
"return_to_amo_overlay",
|
||||
];
|
||||
const FIRST_RUN_TEMPLATES = TEMPLATES_ABOVE_PAGE;
|
||||
const TEMPLATES_ABOVE_PAGE = ["trailhead"];
|
||||
const TEMPLATES_BELOW_SEARCH = ["simple_below_search_snippet"];
|
||||
|
||||
export const ASRouterUtils = {
|
||||
|
@ -48,6 +46,9 @@ export const ASRouterUtils = {
|
|||
dismissById(id) {
|
||||
ASRouterUtils.sendMessage({ type: "DISMISS_MESSAGE_BY_ID", data: { id } });
|
||||
},
|
||||
dismissBundle(bundle) {
|
||||
ASRouterUtils.sendMessage({ type: "DISMISS_BUNDLE", data: { bundle } });
|
||||
},
|
||||
executeAction(button_action) {
|
||||
ASRouterUtils.sendMessage({
|
||||
type: "USER_ACTION",
|
||||
|
@ -108,7 +109,7 @@ export class ASRouterUISurface extends React.PureComponent {
|
|||
this.sendClick = this.sendClick.bind(this);
|
||||
this.sendImpression = this.sendImpression.bind(this);
|
||||
this.sendUserActionTelemetry = this.sendUserActionTelemetry.bind(this);
|
||||
this.state = { message: {} };
|
||||
this.state = { message: {}, bundle: {} };
|
||||
if (props.document) {
|
||||
this.headerPortal = props.document.getElementById(
|
||||
"header-asrouter-container"
|
||||
|
@ -120,10 +121,14 @@ export class ASRouterUISurface extends React.PureComponent {
|
|||
}
|
||||
|
||||
sendUserActionTelemetry(extraProps = {}) {
|
||||
const { message } = this.state;
|
||||
const eventType = `${message.provider}_user_event`;
|
||||
const { message, bundle } = this.state;
|
||||
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({
|
||||
message_id: message.id,
|
||||
message_id: message.id || extraProps.message_id,
|
||||
source: extraProps.id,
|
||||
action: eventType,
|
||||
...extraProps,
|
||||
|
@ -175,6 +180,26 @@ export class ASRouterUISurface extends React.PureComponent {
|
|||
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) {
|
||||
if (id === this.state.message.id) {
|
||||
this.setState({ message: {} });
|
||||
|
@ -188,6 +213,9 @@ export class ASRouterUISurface extends React.PureComponent {
|
|||
case "SET_MESSAGE":
|
||||
this.setState({ message: action.data });
|
||||
break;
|
||||
case "SET_BUNDLED_MESSAGES":
|
||||
this.setState({ bundle: action.data });
|
||||
break;
|
||||
case "CLEAR_MESSAGE":
|
||||
this.clearMessage(action.data.id);
|
||||
break;
|
||||
|
@ -196,8 +224,13 @@ export class ASRouterUISurface extends React.PureComponent {
|
|||
this.setState({ message: {} });
|
||||
}
|
||||
break;
|
||||
case "CLEAR_BUNDLE":
|
||||
if (this.state.bundle.bundle) {
|
||||
this.setState({ bundle: {} });
|
||||
}
|
||||
break;
|
||||
case "CLEAR_ALL":
|
||||
this.setState({ message: {} });
|
||||
this.setState({ message: {}, bundle: {} });
|
||||
break;
|
||||
case "AS_ROUTER_TARGETING_UPDATE":
|
||||
action.data.forEach(id => this.clearMessage(id));
|
||||
|
@ -238,11 +271,18 @@ export class ASRouterUISurface extends React.PureComponent {
|
|||
}
|
||||
|
||||
renderSnippets() {
|
||||
const { message } = this.state;
|
||||
if (!SnippetsTemplates[message.template]) {
|
||||
if (
|
||||
this.state.bundle.template === "onboarding" ||
|
||||
[
|
||||
"fxa_overlay",
|
||||
"return_to_amo_overlay",
|
||||
"trailhead",
|
||||
"whatsnew_panel_message",
|
||||
].includes(this.state.message.template)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const SnippetComponent = SnippetsTemplates[message.template];
|
||||
const SnippetComponent = SnippetsTemplates[this.state.message.template];
|
||||
const { content } = this.state.message;
|
||||
|
||||
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() {
|
||||
if (this.state.message.provider !== "preview") {
|
||||
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() {
|
||||
const { message } = this.state;
|
||||
if (!message.id) {
|
||||
const { message, bundle } = this.state;
|
||||
if (!message.id && !bundle.template) {
|
||||
return null;
|
||||
}
|
||||
const shouldRenderBelowSearch = TEMPLATES_BELOW_SEARCH.includes(
|
||||
|
@ -321,7 +407,9 @@ export class ASRouterUISurface extends React.PureComponent {
|
|||
ReactDOM.createPortal(
|
||||
<>
|
||||
{this.renderPreviewBanner()}
|
||||
{this.renderFirstRun()}
|
||||
{this.renderTrailhead()}
|
||||
{this.renderFirstRunOverlay()}
|
||||
{this.renderOnboarding()}
|
||||
{this.renderSnippets()}
|
||||
</>,
|
||||
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,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { ModalOverlay } from "../../components/ModalOverlay/ModalOverlay";
|
||||
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 {
|
||||
constructor(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 {
|
||||
height: 340px;
|
||||
text-align: center;
|
||||
|
|
|
@ -15,11 +15,8 @@ export class ReturnToAMO extends React.PureComponent {
|
|||
this.onBlockButton = this.onBlockButton.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
global.document.body.classList.add("amo");
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onReady();
|
||||
this.props.sendUserActionTelemetry({
|
||||
event: "IMPRESSION",
|
||||
id: this.props.UISurface,
|
||||
|
|
|
@ -2,15 +2,23 @@
|
|||
* 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 { 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";
|
||||
|
||||
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) {
|
||||
super(props);
|
||||
this.onInputChange = this.onInputChange.bind(this);
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.clickSkip = this.clickSkip.bind(this);
|
||||
this.initScene = this.initScene.bind(this);
|
||||
this.removeOverlay = this.removeOverlay.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";
|
||||
|
||||
this.state = {
|
||||
show: false,
|
||||
emailInput: "",
|
||||
overlayRemoved: false,
|
||||
deviceId: "",
|
||||
flowId: "",
|
||||
flowBeginTime: 0,
|
||||
};
|
||||
this.didFetch = false;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
global.document.body.classList.add("fxa");
|
||||
async componentWillUpdate() {
|
||||
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() {
|
||||
this.initScene();
|
||||
}
|
||||
|
||||
initScene() {
|
||||
// Timeout to allow the scene to render once before attaching the attribute
|
||||
// to trigger the animation.
|
||||
setTimeout(() => {
|
||||
this.setState({ show: true });
|
||||
this.props.onReady();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
|
@ -39,11 +98,11 @@ export class StartupOverlay extends React.PureComponent {
|
|||
window.removeEventListener("visibilitychange", this.removeOverlay);
|
||||
document.body.classList.remove("hide-main", "fxa");
|
||||
this.setState({ show: false });
|
||||
|
||||
this.props.onBlock();
|
||||
setTimeout(() => {
|
||||
// Allow scrolling and fully remove overlay after animation finishes.
|
||||
this.props.onBlock();
|
||||
document.body.classList.remove("welcome");
|
||||
this.setState({ overlayRemoved: true });
|
||||
}, 400);
|
||||
}
|
||||
|
||||
|
@ -73,9 +132,7 @@ export class StartupOverlay extends React.PureComponent {
|
|||
* Report to telemetry additional information about the form submission.
|
||||
*/
|
||||
_getFormInfo() {
|
||||
const value = {
|
||||
has_flow_params: this.props.flowParams.flowId.length > 0,
|
||||
};
|
||||
const value = { has_flow_params: this.state.flowId.length > 0 };
|
||||
return { value };
|
||||
}
|
||||
|
||||
|
@ -88,6 +145,12 @@ export class StartupOverlay extends React.PureComponent {
|
|||
}
|
||||
|
||||
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 (
|
||||
<div className={`overlay-wrapper ${this.state.show ? "show" : ""}`}>
|
||||
<div className="background" />
|
||||
|
@ -150,17 +213,13 @@ export class StartupOverlay extends React.PureComponent {
|
|||
<input
|
||||
name="device_id"
|
||||
type="hidden"
|
||||
value={this.props.flowParams.deviceId}
|
||||
/>
|
||||
<input
|
||||
name="flow_id"
|
||||
type="hidden"
|
||||
value={this.props.flowParams.flowId}
|
||||
value={this.state.deviceId}
|
||||
/>
|
||||
<input name="flow_id" type="hidden" value={this.state.flowId} />
|
||||
<input
|
||||
name="flow_begin_time"
|
||||
type="hidden"
|
||||
value={this.props.flowParams.flowBeginTime}
|
||||
value={this.state.flowBeginTime}
|
||||
/>
|
||||
<span
|
||||
className="error"
|
||||
|
@ -170,7 +229,7 @@ export class StartupOverlay extends React.PureComponent {
|
|||
className="email-input"
|
||||
name="email"
|
||||
type="email"
|
||||
required={true}
|
||||
required="true"
|
||||
onInvalid={this.onInputInvalid}
|
||||
onChange={this.onInputChange}
|
||||
data-l10n-id="onboarding-sync-form-input"
|
||||
|
@ -215,6 +274,5 @@ export class StartupOverlay extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
StartupOverlay.defaultProps = {
|
||||
flowParams: { deviceId: "", flowId: "", flowBeginTime: "" },
|
||||
};
|
||||
const getState = state => ({ fxa_endpoint: state.Prefs.values.fxa_endpoint });
|
||||
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,
|
||||
* 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 { addUtmParams } from "../FirstRun/addUtmParams";
|
||||
import { OnboardingCard } from "../OnboardingMessage/OnboardingMessage";
|
||||
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
|
||||
const FOCUSABLE_SELECTOR = [
|
||||
"a[href]:not([tabindex='-1'])",
|
||||
|
@ -22,35 +29,106 @@ export class Trailhead extends React.PureComponent {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.closeModal = this.closeModal.bind(this);
|
||||
this.hideCardPanel = this.hideCardPanel.bind(this);
|
||||
this.onInputChange = this.onInputChange.bind(this);
|
||||
this.onStartBlur = this.onStartBlur.bind(this);
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.onInputInvalid = this.onInputInvalid.bind(this);
|
||||
this.onCardAction = this.onCardAction.bind(this);
|
||||
|
||||
this.state = {
|
||||
emailInput: "",
|
||||
isModalOpen: true,
|
||||
showCardPanel: true,
|
||||
showCards: false,
|
||||
// The params below are for FxA metrics
|
||||
deviceId: "",
|
||||
flowId: "",
|
||||
flowBeginTime: 0,
|
||||
};
|
||||
this.fxaMetricsInitialized = false;
|
||||
}
|
||||
|
||||
get dialog() {
|
||||
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() {
|
||||
// We need to remove hide-main since we should show it underneath everything that has rendered
|
||||
this.props.document.body.classList.remove("hide-main");
|
||||
|
||||
// The rest of the page is "hidden" to screen readers when the modal is open
|
||||
this.props.document
|
||||
.getElementById("root")
|
||||
.setAttribute("aria-hidden", "true");
|
||||
// Start with focus in the email input box
|
||||
const input = this.dialog.querySelector("input[name=email]");
|
||||
if (input) {
|
||||
input.focus();
|
||||
// Add inline-onboarding class to disable fixed search header and fixed positioned settings icon
|
||||
this.props.document.body.classList.add("inline-onboarding");
|
||||
|
||||
// The rest of the page is "hidden" when the modal is open
|
||||
if (this.props.message.content) {
|
||||
this.props.document
|
||||
.getElementById("root")
|
||||
.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) {
|
||||
let error = e.target.previousSibling;
|
||||
this.setState({ emailInput: e.target.value });
|
||||
|
@ -94,7 +172,8 @@ export class Trailhead extends React.PureComponent {
|
|||
global.removeEventListener("visibilitychange", this.closeModal);
|
||||
this.props.document.body.classList.remove("welcome");
|
||||
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
|
||||
// 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.
|
||||
*/
|
||||
_getFormInfo() {
|
||||
const value = { has_flow_params: this.props.flowParams.flowId.length > 0 };
|
||||
const value = { has_flow_params: this.state.flowId.length > 0 };
|
||||
return { value };
|
||||
}
|
||||
|
||||
|
@ -124,141 +203,234 @@ export class Trailhead extends React.PureComponent {
|
|||
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() {
|
||||
const { props } = this;
|
||||
const { UTMTerm } = props;
|
||||
const { content } = props.message;
|
||||
const { bundle: cards, content, utm_term } = props.message;
|
||||
const innerClassName = ["trailhead", content && content.className]
|
||||
.filter(v => v)
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<ModalOverlayWrapper
|
||||
innerClassName={innerClassName}
|
||||
onClose={this.closeModal}
|
||||
id="trailheadDialog"
|
||||
headerId="trailheadHeader"
|
||||
>
|
||||
<div className="trailheadInner">
|
||||
<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"
|
||||
<>
|
||||
{this.state.isModalOpen && content ? (
|
||||
<ModalOverlayWrapper
|
||||
innerClassName={innerClassName}
|
||||
onClose={this.closeModal}
|
||||
id="trailheadDialog"
|
||||
headerId="trailheadHeader"
|
||||
>
|
||||
<h3
|
||||
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"
|
||||
rel="noopener noreferrer"
|
||||
onSubmit={this.onSubmit}
|
||||
>
|
||||
<input name="service" type="hidden" value="sync" />
|
||||
<input name="action" type="hidden" value="email" />
|
||||
<input name="context" type="hidden" value="fx_desktop_v3" />
|
||||
<input
|
||||
name="entrypoint"
|
||||
type="hidden"
|
||||
value="activity-stream-firstrun"
|
||||
/>
|
||||
<input name="utm_source" type="hidden" value="activity-stream" />
|
||||
<input name="utm_campaign" type="hidden" value="firstrun" />
|
||||
<input name="utm_term" type="hidden" value={UTMTerm} />
|
||||
<input
|
||||
name="device_id"
|
||||
type="hidden"
|
||||
value={this.props.flowParams.deviceId}
|
||||
/>
|
||||
<input
|
||||
name="flow_id"
|
||||
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"
|
||||
<div className="trailheadInner">
|
||||
<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={this.addUtmParams(content.learn.url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
role="group"
|
||||
aria-labelledby="joinFormHeader"
|
||||
aria-describedby="joinFormBody"
|
||||
className="trailheadForm"
|
||||
>
|
||||
<a
|
||||
data-l10n-name="terms"
|
||||
<h3
|
||||
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"
|
||||
rel="noopener noreferrer"
|
||||
href={addUtmParams(
|
||||
"https://accounts.firefox.com/legal/terms",
|
||||
UTMTerm
|
||||
)}
|
||||
/>
|
||||
<a
|
||||
data-l10n-name="privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={addUtmParams(
|
||||
"https://accounts.firefox.com/legal/privacy",
|
||||
UTMTerm
|
||||
)}
|
||||
/>
|
||||
</p>
|
||||
<button
|
||||
data-l10n-id={content.form.button.string_id}
|
||||
type="submit"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
onSubmit={this.onSubmit}
|
||||
>
|
||||
<input name="service" type="hidden" value="sync" />
|
||||
<input name="action" type="hidden" value="email" />
|
||||
<input name="context" type="hidden" value="fx_desktop_v3" />
|
||||
<input
|
||||
name="entrypoint"
|
||||
type="hidden"
|
||||
value="activity-stream-firstrun"
|
||||
/>
|
||||
<input
|
||||
name="utm_source"
|
||||
type="hidden"
|
||||
value="activity-stream"
|
||||
/>
|
||||
<input name="utm_campaign" type="hidden" value="firstrun" />
|
||||
<input name="utm_term" type="hidden" value={utm_term} />
|
||||
<input
|
||||
name="device_id"
|
||||
type="hidden"
|
||||
value={this.state.deviceId}
|
||||
/>
|
||||
<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
|
||||
className="trailheadStart"
|
||||
data-l10n-id={content.skipButton.string_id}
|
||||
onBlur={this.onStartBlur}
|
||||
onClick={this.closeModal}
|
||||
/>
|
||||
</ModalOverlayWrapper>
|
||||
<button
|
||||
className="trailheadStart"
|
||||
data-l10n-id={content.skipButton.string_id}
|
||||
onBlur={this.onStartBlur}
|
||||
onClick={this.closeModal}
|
||||
/>
|
||||
</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) {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
this.onHeaderClick();
|
||||
}
|
||||
}
|
||||
|
@ -224,13 +223,16 @@ export class CollapsibleSection extends React.PureComponent {
|
|||
>
|
||||
{this.renderIcon()}
|
||||
<FluentOrText message={title} />
|
||||
</span>
|
||||
<span
|
||||
className="click-target"
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
onKeyPress={this.onKeyPress}
|
||||
onClick={this.onHeaderClick}
|
||||
>
|
||||
{isCollapsible && (
|
||||
<span
|
||||
data-l10n-id={
|
||||
collapsed
|
||||
? "newtab-section-expand-section-label"
|
||||
: "newtab-section-collapse-section-label"
|
||||
}
|
||||
className={`collapsible-arrow icon ${
|
||||
collapsed
|
||||
? "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.
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/interactive-supports-focus
|
||||
<span className="context-menu">
|
||||
<ul
|
||||
role="menu"
|
||||
onClick={this.onClick}
|
||||
onKeyDown={this.onClick}
|
||||
className="context-menu-list"
|
||||
>
|
||||
<span
|
||||
role="menu"
|
||||
className="context-menu"
|
||||
onClick={this.onClick}
|
||||
onKeyDown={this.onClick}
|
||||
>
|
||||
<ul className="context-menu-list">
|
||||
{this.props.options.map((option, i) =>
|
||||
option.type === "separator" ? (
|
||||
<li key={i} className="separator" role="separator" />
|
||||
<li key={i} className="separator" />
|
||||
) : (
|
||||
option.type !== "empty" && (
|
||||
<ContextMenuItem
|
||||
|
@ -61,6 +61,7 @@ export class ContextMenu extends React.PureComponent {
|
|||
option={option}
|
||||
hideContext={this.hideContext}
|
||||
keyboardAccess={this.props.keyboardAccess}
|
||||
tabIndex="0"
|
||||
/>
|
||||
)
|
||||
)
|
||||
|
@ -76,7 +77,6 @@ export class ContextMenuItem extends React.PureComponent {
|
|||
super(props);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.onKeyUp = this.onKeyUp.bind(this);
|
||||
this.focusFirst = this.focusFirst.bind(this);
|
||||
}
|
||||
|
||||
|
@ -129,8 +129,6 @@ export class ContextMenuItem extends React.PureComponent {
|
|||
this.focusSibling(event.target, event.key);
|
||||
break;
|
||||
case "Enter":
|
||||
case " ":
|
||||
event.preventDefault();
|
||||
this.props.hideContext();
|
||||
option.onClick();
|
||||
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() {
|
||||
const { option } = this.props;
|
||||
return (
|
||||
<li role="presentation" className="context-menu-item">
|
||||
<li role="menuitem" className="context-menu-item">
|
||||
<button
|
||||
className={option.disabled ? "disabled" : ""}
|
||||
role="menuitem"
|
||||
tabIndex="0"
|
||||
onClick={this.onClick}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={this.onKeyUp}
|
||||
ref={option.first ? this.focusFirst : null}
|
||||
>
|
||||
{option.icon && (
|
||||
|
|
|
@ -32,7 +32,7 @@ export class ContextMenuButton extends React.PureComponent {
|
|||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
this.openContextMenu(true, event);
|
||||
}
|
||||
|
|
|
@ -3421,6 +3421,20 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
|
|||
.submissionStatus .submitStatusTitle {
|
||||
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 {
|
||||
height: 340px;
|
||||
text-align: center;
|
||||
|
|
|
@ -3424,6 +3424,20 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
|
|||
.submissionStatus .submitStatusTitle {
|
||||
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 {
|
||||
height: 340px;
|
||||
text-align: center;
|
||||
|
|
|
@ -3421,6 +3421,20 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
|
|||
.submissionStatus .submitStatusTitle {
|
||||
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 {
|
||||
height: 340px;
|
||||
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"],
|
||||
}
|
||||
```
|
||||
|
||||
## 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, {
|
||||
getMessages: this.handleMessageRequest,
|
||||
dispatch: this.dispatch,
|
||||
});
|
||||
|
||||
this._loadLocalProviders();
|
||||
|
@ -900,25 +899,18 @@ class _ASRouter {
|
|||
let interrupt;
|
||||
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(
|
||||
TRAILHEAD_CONFIG.OVERRIDE_PREF,
|
||||
""
|
||||
);
|
||||
if (overrideValue) {
|
||||
[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 || "" };
|
||||
}
|
||||
|
||||
|
@ -984,7 +976,6 @@ class _ASRouter {
|
|||
interrupt,
|
||||
triplet,
|
||||
} = await this._generateTrailheadBranches();
|
||||
|
||||
await this.setState({
|
||||
trailheadInitialized: true,
|
||||
trailheadInterrupt: interrupt,
|
||||
|
@ -1841,6 +1832,11 @@ class _ASRouter {
|
|||
data: { id: action.data.id },
|
||||
});
|
||||
break;
|
||||
case "DISMISS_BUNDLE":
|
||||
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
|
||||
type: "CLEAR_BUNDLE",
|
||||
});
|
||||
break;
|
||||
case "BLOCK_BUNDLE":
|
||||
await this.blockMessageById(action.data.bundle.map(b => b.id));
|
||||
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {
|
||||
|
@ -1895,7 +1891,6 @@ class _ASRouter {
|
|||
break;
|
||||
case "DOORHANGER_TELEMETRY":
|
||||
case "TOOLBAR_BADGE_TELEMETRY":
|
||||
case "TOOLBAR_PANEL_TELEMETRY":
|
||||
if (this.dispatchToAS) {
|
||||
this.dispatchToAS(ac.ASRouterUserEvent(action.data));
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
/* globals Localization */
|
||||
const { FxAccountsConfig } = ChromeUtils.import(
|
||||
"resource://gre/modules/FxAccountsConfig.jsm"
|
||||
);
|
||||
const { AttributionCode } = ChromeUtils.import(
|
||||
"resource:///modules/AttributionCode.jsm"
|
||||
);
|
||||
|
@ -18,7 +21,114 @@ const L10N = new Localization([
|
|||
"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",
|
||||
template: "trailhead",
|
||||
|
@ -326,13 +436,7 @@ const ONBOARDING_MESSAGES = () => [
|
|||
{
|
||||
id: "FXA_1",
|
||||
template: "fxa_overlay",
|
||||
content: {},
|
||||
trigger: { id: "firstRun" },
|
||||
includeBundle: {
|
||||
length: 3,
|
||||
template: "onboarding",
|
||||
trigger: { id: "showOnboarding" },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "RETURN_TO_AMO_1",
|
||||
|
@ -357,11 +461,6 @@ const ONBOARDING_MESSAGES = () => [
|
|||
label: { string_id: "return-to-amo-get-started-button" },
|
||||
},
|
||||
},
|
||||
includeBundle: {
|
||||
length: 3,
|
||||
template: "onboarding",
|
||||
trigger: { id: "showOnboarding" },
|
||||
},
|
||||
targeting:
|
||||
"attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'",
|
||||
trigger: { id: "firstRun" },
|
||||
|
|
|
@ -79,9 +79,9 @@ const MESSAGES = () => [
|
|||
// Never saw this message or saw it in the past 4 days or more recent
|
||||
targeting: `isWhatsNewPanelEnabled &&
|
||||
(earliestFirefoxVersion && firefoxVersion > earliestFirefoxVersion) &&
|
||||
(!messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}'] ||
|
||||
(messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}']|length >= 1 &&
|
||||
currentDate|date - messageImpressions['WHATS_NEW_BADGE_${FIREFOX_VERSION}'][0] <= 4 * 24 * 3600 * 1000))`,
|
||||
messageImpressions[.id == 'WHATS_NEW_BADGE_${FIREFOX_VERSION}']|length == 0 ||
|
||||
(messageImpressions[.id == 'WHATS_NEW_BADGE_${FIREFOX_VERSION}']|length >= 1 &&
|
||||
currentDate|date - messageImpressions[.id == 'WHATS_NEW_BADGE_${FIREFOX_VERSION}'][0] <= 4 * 24 * 3600 * 1000)`,
|
||||
},
|
||||
{
|
||||
id: "WHATS_NEW_70_1",
|
||||
|
|
|
@ -13,11 +13,6 @@ ChromeUtils.defineModuleGetter(
|
|||
"EveryWindow",
|
||||
"resource:///modules/EveryWindow.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm"
|
||||
);
|
||||
|
||||
const WHATSNEW_ENABLED_PREF = "browser.messaging-system.whatsNewPanel.enabled";
|
||||
|
||||
|
@ -29,16 +24,14 @@ const BUTTON_STRING_ID = "cfr-whatsnew-button";
|
|||
|
||||
class _ToolbarPanelHub {
|
||||
constructor() {
|
||||
this.triggerId = "whatsNewPanelOpened";
|
||||
this._showAppmenuButton = this._showAppmenuButton.bind(this);
|
||||
this._hideAppmenuButton = this._hideAppmenuButton.bind(this);
|
||||
this._showToolbarButton = this._showToolbarButton.bind(this);
|
||||
this._hideToolbarButton = this._hideToolbarButton.bind(this);
|
||||
}
|
||||
|
||||
async init(waitForInitialized, { getMessages, dispatch }) {
|
||||
async init(waitForInitialized, { getMessages }) {
|
||||
this._getMessages = getMessages;
|
||||
this._dispatch = dispatch;
|
||||
// Wait for ASRouter messages to become available in order to know
|
||||
// if we can show the What's New panel
|
||||
await waitForInitialized;
|
||||
|
@ -139,36 +132,19 @@ class _ToolbarPanelHub {
|
|||
|
||||
if (messages && !container.querySelector(".whatsNew-message")) {
|
||||
let previousDate = 0;
|
||||
for (let message of messages) {
|
||||
for (let { content } of messages) {
|
||||
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);
|
||||
|
||||
// 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) {
|
||||
const { content } = message;
|
||||
_createMessageElements(win, doc, content, previousDate) {
|
||||
const messageEl = this._createElement(doc, "div");
|
||||
messageEl.classList.add("whatsNew-message");
|
||||
|
||||
|
@ -191,7 +167,7 @@ class _ToolbarPanelHub {
|
|||
csp: null,
|
||||
});
|
||||
|
||||
this.sendUserEventTelemetry(win, "CLICK", message);
|
||||
// TODO: TELEMETRY
|
||||
});
|
||||
|
||||
if (content.icon_url) {
|
||||
|
@ -285,30 +261,6 @@ class _ToolbarPanelHub {
|
|||
_hideElement(document, id) {
|
||||
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;
|
||||
|
|
|
@ -47,7 +47,7 @@ newtab-topsites-save-button = Save
|
|||
newtab-topsites-preview-button = Preview
|
||||
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?
|
||||
# "This action" refers to deleting a page from history.
|
||||
|
@ -89,7 +89,7 @@ newtab-menu-remove-bookmark = Remove Bookmark
|
|||
# Bookmark is a verb here.
|
||||
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".
|
||||
|
||||
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-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.
|
||||
|
||||
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-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.
|
||||
|
||||
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_asrouter_cfr.js]
|
||||
skip-if = fission
|
||||
skip-if = fission
|
||||
[browser_asrouter_bookmarkpanel.js]
|
||||
|
|
|
@ -83,6 +83,9 @@ add_task(async () => {
|
|||
".ReturnToAMOContainer",
|
||||
".ReturnToAMOAddonContents",
|
||||
".ReturnToAMOIcon",
|
||||
// Regular onboarding cards
|
||||
".onboardingMessageContainer",
|
||||
".onboardingMessage",
|
||||
]) {
|
||||
ok(content.document.querySelector(selector), `Should render ${selector}`);
|
||||
}
|
||||
|
|
|
@ -215,7 +215,6 @@ describe("ASRouter", () => {
|
|||
Router.waitForInitialized,
|
||||
{
|
||||
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", () => {
|
||||
it("should remove the id from the messageBlockList", async () => {
|
||||
await Router.onMessage(
|
||||
|
|
|
@ -20,6 +20,21 @@ const FAKE_BELOW_SEARCH_SNIPPET = FAKE_LOCAL_MESSAGES.find(
|
|||
);
|
||||
|
||||
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", () => {
|
||||
let global;
|
||||
|
@ -63,11 +78,6 @@ describe("ASRouterUISurface", () => {
|
|||
location: { href: "" },
|
||||
_listeners: new Set(),
|
||||
_visibilityState: "hidden",
|
||||
head: {
|
||||
appendChild(el) {
|
||||
return el;
|
||||
},
|
||||
},
|
||||
get visibilityState() {
|
||||
return this._visibilityState;
|
||||
},
|
||||
|
@ -88,15 +98,7 @@ describe("ASRouterUISurface", () => {
|
|||
return document.createElement("body");
|
||||
},
|
||||
getElementById(id) {
|
||||
switch (id) {
|
||||
case "header-asrouter-container":
|
||||
return headerPortal;
|
||||
default:
|
||||
return footerPortal;
|
||||
}
|
||||
},
|
||||
createElement(tag) {
|
||||
return document.createElement(tag);
|
||||
return id === "header-asrouter-container" ? headerPortal : footerPortal;
|
||||
},
|
||||
};
|
||||
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", () => {
|
||||
wrapper.setState({ message: { ...FAKE_MESSAGE, provider: "preview" } });
|
||||
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 () => {
|
||||
// wrapper = shallow(<ASRouterUISurface document={fakeDocument} />);
|
||||
const message = (await OnboardingMessageProvider.getUntranslatedMessages()).find(
|
||||
msg => msg.template === "trailhead"
|
||||
);
|
||||
|
||||
wrapper.setState({ message });
|
||||
|
||||
assert.isTrue(headerPortal.childElementCount > 0);
|
||||
assert.equal(footerPortal.childElementCount, 0);
|
||||
});
|
||||
|
@ -366,5 +370,18 @@ describe("ASRouterUISurface", () => {
|
|||
);
|
||||
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 = [
|
||||
{
|
||||
id: "foo",
|
||||
provider: "snippets",
|
||||
template: "simple_snippet",
|
||||
content: { title: "Foo", body: "Foo123" },
|
||||
},
|
||||
{
|
||||
id: "foo1",
|
||||
template: "simple_snippet",
|
||||
provider: "snippets",
|
||||
bundled: 2,
|
||||
order: 1,
|
||||
content: { title: "Foo1", body: "Foo123-1" },
|
||||
|
@ -19,7 +17,6 @@ export const FAKE_LOCAL_MESSAGES = [
|
|||
{
|
||||
id: "foo2",
|
||||
template: "simple_snippet",
|
||||
provider: "snippets",
|
||||
bundled: 2,
|
||||
order: 2,
|
||||
content: { title: "Foo2", body: "Foo123-2" },
|
||||
|
@ -32,19 +29,16 @@ export const FAKE_LOCAL_MESSAGES = [
|
|||
{ id: "baz", content: { title: "Foo", body: "Foo123" } },
|
||||
{
|
||||
id: "newsletter",
|
||||
provider: "snippets",
|
||||
template: "newsletter_snippet",
|
||||
content: { title: "Foo", body: "Foo123" },
|
||||
},
|
||||
{
|
||||
id: "fxa",
|
||||
provider: "snippets",
|
||||
template: "fxa_signup_snippet",
|
||||
content: { title: "Foo", body: "Foo123" },
|
||||
},
|
||||
{
|
||||
id: "belowsearch",
|
||||
provider: "snippets",
|
||||
template: "simple_below_search_snippet",
|
||||
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 { Trailhead } from "content-src/asrouter/templates/Trailhead/Trailhead";
|
||||
|
||||
export const CARDS = [
|
||||
const CARDS = [
|
||||
{
|
||||
content: {
|
||||
title: { string_id: "onboarding-private-browsing-title" },
|
||||
|
@ -27,13 +27,11 @@ describe("<Trailhead>", () => {
|
|||
let dispatch;
|
||||
let onAction;
|
||||
let sandbox;
|
||||
let onNextScene;
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
dispatch = sandbox.stub();
|
||||
onAction = sandbox.stub();
|
||||
onNextScene = sandbox.stub();
|
||||
sandbox.stub(global, "fetch").resolves({
|
||||
ok: true,
|
||||
status: 200,
|
||||
|
@ -61,12 +59,10 @@ describe("<Trailhead>", () => {
|
|||
wrapper = mount(
|
||||
<Trailhead
|
||||
message={message}
|
||||
UTMTerm={message.utm_term}
|
||||
fxaEndpoint="https://accounts.firefox.com/endpoint"
|
||||
dispatch={dispatch}
|
||||
onAction={onAction}
|
||||
document={fakeDocument}
|
||||
onNextScene={onNextScene}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -75,13 +71,11 @@ describe("<Trailhead>", () => {
|
|||
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");
|
||||
assert.ok(skipButton.exists());
|
||||
skipButton.simulate("click");
|
||||
|
||||
assert.calledOnce(onNextScene);
|
||||
|
||||
assert.calledOnce(dispatch);
|
||||
assert.isUserEventAction(dispatch.firstCall.args[0]);
|
||||
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", () => {
|
||||
const skipButton = wrapper.find(".trailheadStart");
|
||||
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");
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const dispatch = sinon.spy();
|
||||
setup({ dispatch });
|
||||
|
|
|
@ -128,10 +128,7 @@ describe("<ContextMenu>", () => {
|
|||
it("should be tabbable", () => {
|
||||
const options = [{ label: "item1", icon: "icon1" }, { type: "separator" }];
|
||||
const wrapper = mount(<ContextMenu {...DEFAULT_PROPS} options={options} />);
|
||||
assert.equal(
|
||||
wrapper.find(".context-menu-item").props().role,
|
||||
"presentation"
|
||||
);
|
||||
assert.equal(wrapper.find(".context-menu-item").props().role, "menuitem");
|
||||
});
|
||||
it("should call onUpdate with false when an option is clicked", () => {
|
||||
const onUpdate = sinon.spy();
|
||||
|
|
|
@ -66,6 +66,10 @@ describe("<ReturnToAMO>", () => {
|
|||
sendUserActionTelemetryStub.reset();
|
||||
});
|
||||
|
||||
it("should call onReady on componentDidMount", () => {
|
||||
assert.calledOnce(onReady);
|
||||
});
|
||||
|
||||
it("should send telemetry on block", () => {
|
||||
wrapper.instance().onBlockButton();
|
||||
|
||||
|
|
|
@ -1,40 +1,44 @@
|
|||
import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
|
||||
import { mount } from "enzyme";
|
||||
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>", () => {
|
||||
let wrapper;
|
||||
let dispatch;
|
||||
let onReady;
|
||||
let onBlock;
|
||||
let sandbox;
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox();
|
||||
sandbox = sinon.sandbox.create();
|
||||
dispatch = sandbox.stub();
|
||||
onReady = sandbox.stub();
|
||||
onBlock = sandbox.stub();
|
||||
|
||||
wrapper = mount(<StartupOverlay onBlock={onBlock} dispatch={dispatch} />);
|
||||
wrapper = mount(
|
||||
<StartupOverlay onBlock={onBlock} onReady={onReady} dispatch={dispatch} />
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
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();
|
||||
wrapper = mount(<StartupOverlay onBlock={onBlock} dispatch={dispatch} />);
|
||||
assert.isFalse(
|
||||
wrapper.find(".overlay-wrapper").hasClass("show"),
|
||||
".overlay-wrapper does not have .show class"
|
||||
wrapper = mount(
|
||||
<StartupOverlay onBlock={onBlock} onReady={onReady} dispatch={dispatch} />
|
||||
);
|
||||
wrapper.setState({ overlayRemoved: false });
|
||||
|
||||
clock.tick(10);
|
||||
wrapper.update();
|
||||
|
||||
assert.isTrue(
|
||||
wrapper.find(".overlay-wrapper").hasClass("show"),
|
||||
".overlay-wrapper has .show class"
|
||||
);
|
||||
assert.calledOnce(onReady);
|
||||
});
|
||||
|
||||
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 getBoolPrefStub;
|
||||
let waitForInitializedStub;
|
||||
let isBrowserPrivateStub;
|
||||
let fakeDispatch;
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.createSandbox();
|
||||
|
@ -30,7 +28,6 @@ describe("ToolbarPanelHub", () => {
|
|||
querySelector: sandbox.stub().returns(null),
|
||||
appendChild: sandbox.stub(),
|
||||
addEventListener: sandbox.stub(),
|
||||
hasAttribute: sandbox.stub(),
|
||||
};
|
||||
fakeDocument = {
|
||||
l10n: {
|
||||
|
@ -61,10 +58,6 @@ describe("ToolbarPanelHub", () => {
|
|||
MozXULElement: { insertFTLIfNeeded: sandbox.stub() },
|
||||
ownerGlobal: {
|
||||
openLinkIn: sandbox.stub(),
|
||||
gBrowser: "gBrowser",
|
||||
},
|
||||
PanelUI: {
|
||||
whatsNewPanel: fakeElementById,
|
||||
},
|
||||
};
|
||||
everyWindowStub = {
|
||||
|
@ -74,8 +67,6 @@ describe("ToolbarPanelHub", () => {
|
|||
addObserverStub = sandbox.stub();
|
||||
removeObserverStub = sandbox.stub();
|
||||
getBoolPrefStub = sandbox.stub();
|
||||
fakeDispatch = sandbox.stub();
|
||||
isBrowserPrivateStub = sandbox.stub();
|
||||
globals.set("EveryWindow", everyWindowStub);
|
||||
globals.set("Services", {
|
||||
...Services,
|
||||
|
@ -85,9 +76,6 @@ describe("ToolbarPanelHub", () => {
|
|||
getBoolPref: getBoolPrefStub,
|
||||
},
|
||||
});
|
||||
globals.set("PrivateBrowsingUtils", {
|
||||
isBrowserPrivate: isBrowserPrivateStub,
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
instance.uninit();
|
||||
|
@ -97,10 +85,10 @@ describe("ToolbarPanelHub", () => {
|
|||
it("should create an 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);
|
||||
instance.enableAppmenuButton = sandbox.stub();
|
||||
await instance.init(waitForInitializedStub, { getMessages: () => {} });
|
||||
instance.init(waitForInitializedStub, { getMessages: () => {} });
|
||||
assert.notCalled(instance.enableAppmenuButton);
|
||||
});
|
||||
it("should enableAppmenuButton() on init() if pref is enabled", async () => {
|
||||
|
@ -227,221 +215,85 @@ describe("ToolbarPanelHub", () => {
|
|||
instance._hideToolbarButton(fakeWindow);
|
||||
assert.calledWith(fakeElementById.setAttribute, "hidden", true);
|
||||
});
|
||||
describe("#renderMessages", () => {
|
||||
let getMessagesStub;
|
||||
beforeEach(() => {
|
||||
getMessagesStub = sandbox.stub();
|
||||
instance.init(waitForInitializedStub, {
|
||||
getMessages: getMessagesStub,
|
||||
dispatch: fakeDispatch,
|
||||
});
|
||||
it("should render messages to the panel on renderMessages()", async () => {
|
||||
const messages = (await PanelTestProvider.getMessages()).filter(
|
||||
m => m.template === "whatsnew_panel_message"
|
||||
);
|
||||
messages[0].content.link_text = { string_id: "link_text_id" };
|
||||
instance.init(waitForInitializedStub, {
|
||||
getMessages: sandbox
|
||||
.stub()
|
||||
.returns([messages[0], messages[2], messages[1]]),
|
||||
});
|
||||
it("should render messages to the panel on renderMessages()", async () => {
|
||||
const messages = (await PanelTestProvider.getMessages()).filter(
|
||||
m => m.template === "whatsnew_panel_message"
|
||||
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
||||
for (let message of messages) {
|
||||
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) {
|
||||
assert.ok(
|
||||
createdElements.find(
|
||||
el =>
|
||||
el.tagName === "h2" && el.textContent === message.content.title
|
||||
)
|
||||
);
|
||||
assert.ok(
|
||||
createdElements.find(
|
||||
el => el.tagName === "p" && el.textContent === message.content.body
|
||||
)
|
||||
);
|
||||
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
||||
|
||||
assert.calledOnce(fakeElementById.addEventListener);
|
||||
assert.calledWithExactly(
|
||||
fakeElementById.addEventListener,
|
||||
"popuphidden",
|
||||
sinon.match.func,
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
// 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)),
|
||||
];
|
||||
getMessagesStub.returns(messages);
|
||||
);
|
||||
const [, cb] = fakeElementById.addEventListener.firstCall.args;
|
||||
|
||||
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
||||
assert.notCalled(everyWindowStub.unregisterCallback);
|
||||
|
||||
assert.callCount(instance._createDateElement, uniqueDates.length);
|
||||
});
|
||||
it("should listen for panelhidden and remove the toolbar button", async () => {
|
||||
getMessagesStub.returns([]);
|
||||
fakeDocument.getElementById
|
||||
.withArgs("customizationui-widget-panel")
|
||||
.returns(null);
|
||||
cb();
|
||||
|
||||
await instance.renderMessages(fakeWindow, fakeDocument, "container-id");
|
||||
|
||||
assert.notCalled(fakeElementById.addEventListener);
|
||||
});
|
||||
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"
|
||||
);
|
||||
});
|
||||
});
|
||||
assert.calledOnce(everyWindowStub.unregisterCallback);
|
||||
assert.calledWithExactly(
|
||||
everyWindowStub.unregisterCallback,
|
||||
"whats-new-menu-button"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -47,7 +47,7 @@ newtab-topsites-save-button = Save
|
|||
newtab-topsites-preview-button = Preview
|
||||
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?
|
||||
# "This action" refers to deleting a page from history.
|
||||
|
@ -89,7 +89,7 @@ newtab-menu-remove-bookmark = Remove Bookmark
|
|||
# Bookmark is a verb here.
|
||||
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".
|
||||
|
||||
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-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.
|
||||
|
||||
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-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.
|
||||
|
||||
newtab-section-header-topsites = Top Sites
|
||||
|
|
Загрузка…
Ссылка в новой задаче