• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / fx-private-relay / 6b43dd79-8f94-4235-acd1-94524bacef54

02 Jul 2025 04:04PM UTC coverage: 86.642% (+1.3%) from 85.326%
6b43dd79-8f94-4235-acd1-94524bacef54

Pull #5647

circleci

vpremamozilla
MPP-4153 - test(mask-management): increase frontend test coverage for Mask Management pages and components
Pull Request #5647: MPP-4153 - increase frontend test coverage for mask management

2586 of 3657 branches covered (70.71%)

Branch coverage included in aggregate %.

17846 of 19925 relevant lines covered (89.57%)

10.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.38
/frontend/src/components/layout/navigation/AppPicker.tsx
1
import {
2
  useMenuTriggerState,
3
  useTreeState,
4
  TreeProps,
5
  TreeState,
6
  Item,
7
} from "react-stately";
8✔
8
import {
9
  useMenuTrigger,
10
  useButton,
11
  useOverlay,
12
  FocusScope,
13
  DismissButton,
14
  mergeProps,
15
  useMenuItem,
16
  useFocus,
17
} from "react-aria";
8✔
18
import { Key, ReactNode, useRef, useState, useEffect, RefObject } from "react";
8✔
19
import { AriaMenuItemProps } from "@react-aria/menu";
20
import styles from "./AppPicker.module.scss";
8✔
21
import FirefoxLogo from "../images/fx.png";
8✔
22
import MonitorLogo from "../images/monitor.png";
8✔
23
import PocketLogo from "../images/pocket.png";
8✔
24
import VpnLogo from "../images/vpn.svg";
8✔
25
import FxDesktopLogo from "../images/fx-logo.svg";
8✔
26
import FxMobileLogo from "../images/fx-mobile.png";
8✔
27
import { Props as LayoutProps } from "../Layout";
28
import { getRuntimeConfig } from "../../../config";
8✔
29
import { BentoIcon } from "../../Icons";
8✔
30
import Image from "../../Image";
8✔
31
import { useGaEvent } from "../../../hooks/gaEvent";
8✔
32
import { useL10n } from "../../../hooks/l10n";
8✔
33
import { MenuPopupProps, useMenu } from "../../../hooks/menu";
8✔
34

35
const getProducts = (referringSiteUrl: string) => ({
97✔
36
  monitor: {
37
    id: "monitor",
38
    url: `https://monitor.firefox.com/?utm_source=${encodeURIComponent(
39
      referringSiteUrl,
40
    )}&utm_medium=referral&utm_campaign=bento&utm_content=desktop&prompt=none`,
41
    gaLabel: "moz-monitor",
42
  },
43
  pocket: {
44
    id: "pocket",
45
    url: "https://app.adjust.com/hr2n0yz?engagement_type=fallback_click&fallback=https%3A%2F%2Fgetpocket.com%2Ffirefox_learnmore%3Fsrc%3Dff_bento&fallback_lp=https%3A%2F%2Fapps.apple.com%2Fapp%2Fpocket-save-read-grow%2Fid309601447",
46
    gaLabel: "pocket",
47
  },
48
  fxDesktop: {
49
    id: "fxDesktop",
50
    url: `https://www.mozilla.org/firefox/new/?utm_source=${encodeURIComponent(
51
      referringSiteUrl,
52
    )}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`,
53
    gaLabel: "fx-desktop",
54
  },
55
  fxMobile: {
56
    id: "fxMobile",
57
    url: `https://www.mozilla.org/firefox/browsers/mobile/?utm_source=${encodeURIComponent(
58
      referringSiteUrl,
59
    )}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`,
60
    gaLabel: "fx-mobile",
61
  },
62
  vpn: {
63
    id: "vpn",
64
    url: `https://www.mozilla.org/products/vpn/?utm_source=${encodeURIComponent(
65
      referringSiteUrl,
66
    )}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`,
67
    gaLabel: "vpn",
68
  },
69
});
70

71
export type Props = {
72
  theme?: LayoutProps["theme"];
73
  style: string;
74
};
75

76
/**
77
 * Menu that can be opened to see other relevant products Mozilla has available for people.
78
 */
79
export const AppPicker = (props: Props) => {
97✔
80
  const l10n = useL10n();
97✔
81
  const gaEvent = useGaEvent();
97✔
82

83
  const products = getProducts(
97✔
84
    typeof document !== "undefined"
97!
85
      ? document.location.host
86
      : "relay.firefox.com",
87
  );
88
  const linkRefs: Record<
89
    keyof typeof products,
90
    RefObject<HTMLAnchorElement | null>
91
  > = {
97✔
92
    monitor: useRef<HTMLAnchorElement>(null),
93
    pocket: useRef<HTMLAnchorElement>(null),
94
    fxDesktop: useRef<HTMLAnchorElement>(null),
95
    fxMobile: useRef<HTMLAnchorElement>(null),
96
    vpn: useRef<HTMLAnchorElement>(null),
97
  };
98
  const mozillaLinkRef = useRef<HTMLAnchorElement>(null);
97✔
99

100
  const onSelect = (itemKey: Key) => {
97✔
101
    Object.entries(products).forEach(([key, productData]) => {
×
102
      if (itemKey === productData.id) {
×
103
        linkRefs[key as keyof typeof products].current?.click();
×
104
        gaEvent({
×
105
          category: "bento",
106
          action: "bento-app-link-click",
107
          label: productData.gaLabel,
108
        });
109
      }
110
    });
111
    if (itemKey === "mozilla") {
×
112
      mozillaLinkRef.current?.click();
×
113
      gaEvent({
×
114
        category: "bento",
115
        action: "bento-app-link-click",
116
        label: "Mozilla",
117
      });
118
    }
119
  };
120

121
  return (
122
    <AppPickerTrigger
123
      label={l10n.getString("bento-button-title")}
124
      onAction={onSelect}
125
      theme={props.theme}
126
      style={props.style}
127
    >
128
      <Item key={products.vpn.id} textValue={l10n.getString("fx-vpn")}>
129
        <a
130
          ref={linkRefs.vpn}
131
          href={products.vpn.url}
132
          className={`${styles["menu-link"]} ${styles["vpn-link"]}`}
133
          target="_blank"
134
          rel="noopener noreferrer"
135
        >
136
          <Image src={VpnLogo} alt="" width={16} height={16} />
137
          {l10n.getString("fx-vpn")}
138
        </a>
139
      </Item>
140
      <Item key={products.monitor.id} textValue={l10n.getString("moz-monitor")}>
141
        <a
142
          ref={linkRefs.monitor}
143
          href={products.monitor.url}
144
          className={`${styles["menu-link"]} ${styles["monitor-link"]}`}
145
          target="_blank"
146
          rel="noopener noreferrer"
147
        >
148
          <Image src={MonitorLogo} alt="" width={16} height={16} />
149
          {l10n.getString("moz-monitor")}
150
        </a>
151
      </Item>
152
      <Item key={products.pocket.id} textValue={l10n.getString("fx-pocket")}>
153
        <a
154
          ref={linkRefs.pocket}
155
          href={products.pocket.url}
156
          className={`${styles["menu-link"]} ${styles["pocket-link"]}`}
157
          target="_blank"
158
          rel="noopener noreferrer"
159
        >
160
          <Image src={PocketLogo} alt="" width={16} height={16} />
161
          {l10n.getString("fx-pocket")}
162
        </a>
163
      </Item>
164
      <Item
165
        key={products.fxDesktop.id}
166
        textValue={l10n.getString("fx-desktop-2")}
167
      >
168
        <a
169
          ref={linkRefs.fxDesktop}
170
          href={products.fxDesktop.url}
171
          className={`${styles["menu-link"]} ${styles["fx-desktop-link"]}`}
172
          target="_blank"
173
          rel="noopener noreferrer"
174
        >
175
          <Image src={FxDesktopLogo} alt="" width={16} height={16} />
176
          {l10n.getString("fx-desktop-2")}
177
        </a>
178
      </Item>
179
      <Item
180
        key={products.fxMobile.id}
181
        textValue={l10n.getString("fx-mobile-2")}
182
      >
183
        <a
184
          ref={linkRefs.fxMobile}
185
          href={products.fxMobile.url}
186
          className={`${styles["menu-link"]} ${styles["fx-mobile-link"]}`}
187
          target="_blank"
188
          rel="noopener noreferrer"
189
        >
190
          <Image src={FxMobileLogo} alt="" width={16} height={16} />
191
          {l10n.getString("fx-mobile-2")}
192
        </a>
193
      </Item>
194

195
      <Item key="mozilla" textValue={l10n.getString("made-by-mozilla")}>
196
        <a
197
          ref={mozillaLinkRef}
198
          href={`https://www.mozilla.org/?utm_source=${encodeURIComponent(
199
            getRuntimeConfig().frontendOrigin,
200
          )}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`}
201
          className={`${styles["menu-link"]} ${styles["mozilla-link"]}`}
202
          target="_blank"
203
          rel="noopener noreferrer"
204
        >
205
          {l10n.getString("made-by-mozilla")}
206
        </a>
207
      </Item>
208
    </AppPickerTrigger>
209
  );
210
};
211

212
type AppPickerTriggerProps = Parameters<typeof useMenuTriggerState>[0] & {
213
  label: string;
214
  style: string;
215
  children: TreeProps<Record<string, never>>["children"];
216
  onAction: AriaMenuItemProps["onAction"];
217
  theme?: LayoutProps["theme"];
218
};
219
const AppPickerTrigger = ({
8✔
220
  label,
221
  theme,
222
  style,
223
  ...otherProps
224
}: AppPickerTriggerProps) => {
225
  const l10n = useL10n();
97✔
226
  const appPickerTriggerState = useMenuTriggerState(otherProps);
97✔
227
  const isFirstRenderDone = useRef(false);
97✔
228
  const gaEvent = useGaEvent();
97✔
229

230
  const triggerButtonRef = useRef<HTMLButtonElement>(null);
97✔
231
  const { menuTriggerProps, menuProps } = useMenuTrigger(
97✔
232
    {},
233
    appPickerTriggerState,
234
    triggerButtonRef,
235
  );
236
  // `menuProps` has an `autoFocus` property that is not compatible with the
237
  // `autoFocus` property for HTMLElements, because it can also be of type
238
  // `FocusStrategy` (i.e. the string "first" or "last") at the time of writing.
239
  // Since its values get spread onto an HTMLDivElement, we ignore those values.
240
  // See
241
  // https://github.com/mozilla/fx-private-relay/pull/3261#issuecomment-1493840024
242
  const menuPropsWithoutAutofocus = {
97✔
243
    ...menuProps,
244
    autoFocus:
245
      typeof menuProps.autoFocus === "boolean"
97!
246
        ? menuProps.autoFocus
247
        : undefined,
248
  };
249

250
  const triggerButtonProps = useButton(
97✔
251
    menuTriggerProps,
252
    triggerButtonRef,
253
  ).buttonProps;
254

255
  useEffect(() => {
97✔
256
    if (!isFirstRenderDone.current) {
84✔
257
      isFirstRenderDone.current = true;
84✔
258
      return;
84✔
259
    }
260
    gaEvent({
×
261
      category: "bento",
262
      action: appPickerTriggerState.isOpen ? "bento-opened" : "bento-closed",
×
263
      label: getRuntimeConfig().frontendOrigin,
264
    });
265
  }, [appPickerTriggerState.isOpen, gaEvent]);
266

267
  return (
268
    <div className={`${styles.wrapper} ${style}`}>
269
      <button
270
        {...triggerButtonProps}
271
        ref={triggerButtonRef}
272
        title={l10n.getString("bento-button-title")}
273
        className={`${styles.trigger} ${
274
          theme === "premium" ? styles["is-premium"] : styles["is-free"]
97✔
275
        }`}
276
      >
277
        <BentoIcon
278
          alt={label}
279
          className={`${theme === "premium" ? styles.premium : ""}`}
97✔
280
        />
281
      </button>
282
      {appPickerTriggerState.isOpen && (
97✔
283
        <AppPickerPopup
284
          {...otherProps}
285
          aria-label={l10n.getString("bento-button-title")}
286
          domProps={menuPropsWithoutAutofocus}
287
          autoFocus={appPickerTriggerState.focusStrategy}
288
          onClose={() => appPickerTriggerState.close()}
×
289
        />
290
      )}
291
    </div>
292
  );
293
};
294

295
type AppPickerPopupProps = MenuPopupProps<Record<string, never>>;
296
const AppPickerPopup = (props: AppPickerPopupProps) => {
8✔
297
  const l10n = useL10n();
×
298
  const popupState = useTreeState({ ...props, selectionMode: "none" });
×
299

300
  const popupRef = useRef<HTMLDivElement>(null);
×
301
  const popupProps = useMenu(props, popupState, popupRef).menuProps;
×
302

303
  const overlayRef = useRef<HTMLDivElement>(null);
×
304
  const { overlayProps } = useOverlay(
×
305
    {
306
      onClose: props.onClose,
307
      shouldCloseOnBlur: true,
308
      isOpen: true,
309
      isDismissable: true,
310
    },
311
    overlayRef,
312
  );
313

314
  // <FocusScope> ensures that focus is restored back to the
315
  // trigger when the menu is closed.
316
  // The <DismissButton> components allow screen reader users
317
  // to dismiss the popup easily.
318
  return (
319
    <FocusScope restoreFocus>
320
      <div {...overlayProps} ref={overlayRef}>
321
        <DismissButton onDismiss={props.onClose} />
322
        <div
323
          {...mergeProps(popupProps, props.domProps)}
324
          ref={popupRef}
325
          className={styles.popup}
326
        >
327
          <div className={styles["app-picker-heading"]}>
328
            <Image src={FirefoxLogo} alt="" width={32} height={32} />
329
            <h2>{l10n.getString("fx-makes-tech")}</h2>
330
          </div>
331
          <ul>
332
            {Array.from(popupState.collection).map((item) => (
333
              <AppPickerItem
×
334
                key={item.key}
335
                // TODO: Fix the typing (likely: report to react-aria that the type does not include an isDisabled prop)
336
                item={item as unknown as AppPickerItemProps["item"]}
337
                state={popupState}
338
                onAction={props.onAction}
339
                onClose={props.onClose}
340
              />
341
            ))}
342
          </ul>
343
        </div>
344
        <DismissButton onDismiss={props.onClose} />
345
      </div>
346
    </FocusScope>
347
  );
348
};
349

350
type AppPickerItemProps = {
351
  // TODO: Figure out correct type:
352
  item: {
353
    key: AriaMenuItemProps["key"];
354
    isDisabled: AriaMenuItemProps["isDisabled"];
355
    rendered?: ReactNode;
356
  };
357
  state: TreeState<unknown>;
358
  onAction: AriaMenuItemProps["onAction"];
359
  onClose: AriaMenuItemProps["onClose"];
360
};
361

362
const AppPickerItem = (props: AppPickerItemProps) => {
8✔
363
  const menuItemRef = useRef<HTMLLIElement>(null);
×
364
  const menuItemProps = useMenuItem(
×
365
    {
366
      key: props.item.key,
367
      isDisabled: props.item.isDisabled,
368
      onAction: props.onAction,
369
      onClose: props.onClose,
370
    },
371
    props.state,
372
    menuItemRef,
373
  ).menuItemProps;
374

375
  const [_isFocused, setIsFocused] = useState(false);
×
376
  const focusProps = useFocus({ onFocusChange: setIsFocused }).focusProps;
×
377

378
  return (
379
    <li
380
      {...mergeProps(menuItemProps, focusProps)}
381
      ref={menuItemRef}
382
      className={styles["menu-item-wrapper"]}
383
    >
384
      {props.item.rendered}
385
    </li>
386
  );
387
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc