Bug 1776202 - Pocket newtab recent saves adding my list link to top right of section r=gvn
authorscott <scott.downe@gmail.com>
Fri, 01 Jul 2022 16:20:38 +0000
changeset 4480201 3bdb7367a3c9fb0d38cf33a7a762e347b798b436
parent 4480200 81a09589dba32fda0999bfe350e2bb89f05ffcab
child 4480202 9ae1bb0f40eb603a61ca2e3581848ebce6582325
push id827486
push userwptsync@mozilla.com
push dateSat, 02 Jul 2022 02:04:09 +0000
treeherdertry@16a4e1535415 [default view] [failures only]
reviewersgvn
bugs1776202
milestone104.0a1
Bug 1776202 - Pocket newtab recent saves adding my list link to top right of section r=gvn Differential Revision: https://phabricator.services.mozilla.com/D150500
browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/_CardGrid.scss
browser/components/newtab/css/activity-stream-linux.css
browser/components/newtab/css/activity-stream-mac.css
browser/components/newtab/css/activity-stream-windows.css
browser/components/newtab/data/content/activity-stream.bundle.js
browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
@@ -4,48 +4,33 @@
 
 import {
   DSCard,
   PlaceholderDSCard,
   LastCardMessage,
 } from "../DSCard/DSCard.jsx";
 import { DSEmptyState } from "../DSEmptyState/DSEmptyState.jsx";
 import { TopicsWidget } from "../TopicsWidget/TopicsWidget.jsx";
+import { SafeAnchor } from "../SafeAnchor/SafeAnchor";
 import { FluentOrText } from "../../FluentOrText/FluentOrText.jsx";
 import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
 import React, { useEffect, useState, useRef, useCallback } from "react";
 import { connect, useSelector } from "react-redux";
 const WIDGET_IDS = {
   TOPICS: 1,
 };
 
-export function DSSubHeader(props) {
+export function DSSubHeader({ children }) {
   return (
     <div className="section-top-bar ds-sub-header">
-      <h3 className="section-title-container">
-        <span className="section-title">{props.children}</span>
-      </h3>
+      <h3 className="section-title-container">{children}</h3>
     </div>
   );
 }
 
-export function GridContainer(props) {
-  const { header, className, children } = props;
-  return (
-    <>
-      {header && (
-        <DSSubHeader>
-          <FluentOrText message={header} />
-        </DSSubHeader>
-      )}
-      <div className={`ds-card-grid ${className}`}>{children}</div>
-    </>
-  );
-}
-
 export function IntersectionObserver({
   children,
   windowObj = window,
   onIntersecting,
 }) {
   const intersectionElement = useRef(null);
 
   useEffect(() => {
@@ -68,24 +53,27 @@ export function IntersectionObserver({
     // Cleanup
     return () => observer?.disconnect();
   }, [windowObj, onIntersecting]);
 
   return <div ref={intersectionElement}>{children}</div>;
 }
 
 export function RecentSavesContainer({
-  className,
+  gridClassName = "",
   dispatch,
   windowObj = window,
   items = 3,
+  source = "CARDGRID_RECENT_SAVES",
 }) {
-  const { recentSavesData, isUserLoggedIn } = useSelector(
-    state => state.DiscoveryStream
-  );
+  const {
+    recentSavesData,
+    isUserLoggedIn,
+    experimentData: { utmCampaign, utmContent, utmSource },
+  } = useSelector(state => state.DiscoveryStream);
 
   const [visible, setVisible] = useState(false);
   const onIntersecting = useCallback(() => setVisible(true), []);
 
   useEffect(() => {
     if (visible) {
       dispatch(
         ac.AlsoToMain({
@@ -112,31 +100,44 @@ export function RecentSavesContainer({
   }
 
   function renderCard(rec, index) {
     return (
       <DSCard
         key={`dscard-${rec?.id || index}`}
         id={rec.id}
         pos={index}
-        type="CARDGRID_RECENT_SAVES"
+        type={source}
         image_src={rec.image_src}
         raw_image_src={rec.raw_image_src}
         word_count={rec.word_count}
         time_to_read={rec.time_to_read}
         title={rec.title}
         excerpt={rec.excerpt}
         url={rec.url}
         source={rec.domain}
         isRecentSave={true}
         dispatch={dispatch}
       />
     );
   }
 
+  function onMyListClicked() {
+    dispatch(
+      ac.UserEvent({
+        event: "CLICK",
+        source: `${source}_VIEW_LIST`,
+      })
+    );
+  }
+
+  let queryParams = `?utm_source=${utmSource}`;
+  if (utmCampaign && utmContent) {
+    queryParams += `&utm_content=${utmContent}&utm_campaign=${utmCampaign}`;
+  }
   const recentSavesCards = [];
   // We fill the cards with a for loop over an inline map because
   // we want empty placeholders if there are not enough cards.
   for (let index = 0; index < items; index++) {
     const recentSave = recentSavesData[index];
     if (!recentSave) {
       recentSavesCards.push(<PlaceholderDSCard key={`dscard-${index}`} />);
     } else {
@@ -156,19 +157,31 @@ export function RecentSavesContainer({
           index
         )
       );
     }
   }
 
   // We are visible and logged in.
   return (
-    <GridContainer className={className} header="Recently Saved to your List">
-      {recentSavesCards}
-    </GridContainer>
+    <>
+      <DSSubHeader>
+        <span className="section-title">
+          <FluentOrText message="Recently Saved to your List" />
+        </span>
+        <SafeAnchor
+          onLinkClick={onMyListClicked}
+          className="section-sub-link"
+          url={`https://getpocket.com/a${queryParams}`}
+        >
+          <FluentOrText message="View My List" />
+        </SafeAnchor>
+      </DSSubHeader>
+      <div className={gridClassName}>{recentSavesCards}</div>
+    </>
   );
 }
 
 export class _CardGrid extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = { moreLoaded: false };
     this.loadMoreClicked = this.loadMoreClicked.bind(this);
@@ -353,40 +366,50 @@ export class _CardGrid extends React.Pur
     const hideDescriptionsClassName = !hideDescriptions
       ? `ds-card-grid-include-descriptions`
       : ``;
     const compactGridClassName = compactGrid ? `ds-card-grid-compact` : ``;
     const hybridLayoutClassName = hybridLayout
       ? `ds-card-grid-hybrid-layout`
       : ``;
 
-    const className = `ds-card-grid-${this.props.border} ${variantClass} ${hybridLayoutClassName} ${hideCardBackgroundClass} ${fourCardLayoutClass} ${hideDescriptionsClassName} ${compactGridClassName}`;
+    const gridClassName = `ds-card-grid ds-card-grid-${this.props.border} ${variantClass} ${hybridLayoutClassName} ${hideCardBackgroundClass} ${fourCardLayoutClass} ${hideDescriptionsClassName} ${compactGridClassName}`;
 
     return (
       <>
         {essentialReadsCards?.length > 0 && (
-          <GridContainer className={className}>
-            {essentialReadsCards}
-          </GridContainer>
+          <div className={gridClassName}>{essentialReadsCards}</div>
         )}
         {showRecentSaves && (
           <RecentSavesContainer
-            className={className}
+            gridClassName={gridClassName}
             dispatch={this.props.dispatch}
           />
         )}
         {editorsPicksCards?.length > 0 && (
-          <GridContainer className={className} header="Editor’s Picks">
-            {editorsPicksCards}
-          </GridContainer>
+          <>
+            <DSSubHeader>
+              <span className="section-title">
+                <FluentOrText message="Editor’s Picks" />
+              </span>
+            </DSSubHeader>
+            <div className={gridClassName}>{editorsPicksCards}</div>
+          </>
         )}
         {cards?.length > 0 && (
-          <GridContainer className={className} header={moreRecsHeader}>
-            {cards}
-          </GridContainer>
+          <>
+            {moreRecsHeader && (
+              <DSSubHeader>
+                <span className="section-title">
+                  <FluentOrText message={moreRecsHeader} />
+                </span>
+              </DSSubHeader>
+            )}
+            <div className={gridClassName}>{cards}</div>
+          </>
         )}
       </>
     );
   }
 
   render() {
     const { data } = this.props;
 
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/_CardGrid.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/_CardGrid.scss
@@ -283,16 +283,38 @@
       padding: 12px 0 0;
     }
   }
 }
 
 .ds-layout {
   .ds-sub-header {
     margin-top: 24px;
+
+    .section-title-container {
+      flex-direction: row;
+      align-items: baseline;
+      justify-content: space-between;
+      display: flex;
+    }
+
+    .section-sub-link {
+      color: var(--newtab-primary-action-background);
+      font-size: 14px;
+      line-height: 16px;
+      cursor: pointer;
+
+      &:hover {
+        text-decoration: underline;
+      }
+
+      &:active {
+        color: var(--newtab-primary-element-active-color);
+      }
+    }
   }
 
   .ds-card-grid-load-more-button {
     display: block;
     margin: 32px auto 0;
     font-size: 13px;
     line-height: 16px;
     border-radius: 4px;
--- a/browser/components/newtab/css/activity-stream-linux.css
+++ b/browser/components/newtab/css/activity-stream-linux.css
@@ -2945,16 +2945,34 @@ main.has-snippet {
 .outer-wrapper .ds-card-grid.ds-card-grid-hide-background.ds-card-grid-border .ds-card:not(.placeholder) .meta,
 .outer-wrapper.newtab-experience .ds-card-grid.ds-card-grid-hide-background.ds-card-grid-border .ds-card:not(.placeholder) .meta {
   padding: 12px 0 0;
 }
 
 .ds-layout .ds-sub-header {
   margin-top: 24px;
 }
+.ds-layout .ds-sub-header .section-title-container {
+  flex-direction: row;
+  align-items: baseline;
+  justify-content: space-between;
+  display: flex;
+}
+.ds-layout .ds-sub-header .section-sub-link {
+  color: var(--newtab-primary-action-background);
+  font-size: 14px;
+  line-height: 16px;
+  cursor: pointer;
+}
+.ds-layout .ds-sub-header .section-sub-link:hover {
+  text-decoration: underline;
+}
+.ds-layout .ds-sub-header .section-sub-link:active {
+  color: var(--newtab-primary-element-active-color);
+}
 .ds-layout .ds-card-grid-load-more-button {
   display: block;
   margin: 32px auto 0;
   font-size: 13px;
   line-height: 16px;
   border-radius: 4px;
 }
 
--- a/browser/components/newtab/css/activity-stream-mac.css
+++ b/browser/components/newtab/css/activity-stream-mac.css
@@ -2949,16 +2949,34 @@ main.has-snippet {
 .outer-wrapper .ds-card-grid.ds-card-grid-hide-background.ds-card-grid-border .ds-card:not(.placeholder) .meta,
 .outer-wrapper.newtab-experience .ds-card-grid.ds-card-grid-hide-background.ds-card-grid-border .ds-card:not(.placeholder) .meta {
   padding: 12px 0 0;
 }
 
 .ds-layout .ds-sub-header {
   margin-top: 24px;
 }
+.ds-layout .ds-sub-header .section-title-container {
+  flex-direction: row;
+  align-items: baseline;
+  justify-content: space-between;
+  display: flex;
+}
+.ds-layout .ds-sub-header .section-sub-link {
+  color: var(--newtab-primary-action-background);
+  font-size: 14px;
+  line-height: 16px;
+  cursor: pointer;
+}
+.ds-layout .ds-sub-header .section-sub-link:hover {
+  text-decoration: underline;
+}
+.ds-layout .ds-sub-header .section-sub-link:active {
+  color: var(--newtab-primary-element-active-color);
+}
 .ds-layout .ds-card-grid-load-more-button {
   display: block;
   margin: 32px auto 0;
   font-size: 13px;
   line-height: 16px;
   border-radius: 4px;
 }
 
--- a/browser/components/newtab/css/activity-stream-windows.css
+++ b/browser/components/newtab/css/activity-stream-windows.css
@@ -2945,16 +2945,34 @@ main.has-snippet {
 .outer-wrapper .ds-card-grid.ds-card-grid-hide-background.ds-card-grid-border .ds-card:not(.placeholder) .meta,
 .outer-wrapper.newtab-experience .ds-card-grid.ds-card-grid-hide-background.ds-card-grid-border .ds-card:not(.placeholder) .meta {
   padding: 12px 0 0;
 }
 
 .ds-layout .ds-sub-header {
   margin-top: 24px;
 }
+.ds-layout .ds-sub-header .section-title-container {
+  flex-direction: row;
+  align-items: baseline;
+  justify-content: space-between;
+  display: flex;
+}
+.ds-layout .ds-sub-header .section-sub-link {
+  color: var(--newtab-primary-action-background);
+  font-size: 14px;
+  line-height: 16px;
+  cursor: pointer;
+}
+.ds-layout .ds-sub-header .section-sub-link:hover {
+  text-decoration: underline;
+}
+.ds-layout .ds-sub-header .section-sub-link:active {
+  color: var(--newtab-primary-element-active-color);
+}
 .ds-layout .ds-card-grid-load-more-button {
   display: block;
   margin: 32px auto 0;
   font-size: 13px;
   line-height: 16px;
   border-radius: 4px;
 }
 
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -8145,38 +8145,27 @@ const TopicsWidget = (0,external_ReactRe
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 
 
 
 
 
+
 const WIDGET_IDS = {
   TOPICS: 1
 };
-function DSSubHeader(props) {
+function DSSubHeader({
+  children
+}) {
   return /*#__PURE__*/external_React_default().createElement("div", {
     className: "section-top-bar ds-sub-header"
   }, /*#__PURE__*/external_React_default().createElement("h3", {
     className: "section-title-container"
-  }, /*#__PURE__*/external_React_default().createElement("span", {
-    className: "section-title"
-  }, props.children)));
-}
-function GridContainer(props) {
-  const {
-    header,
-    className,
-    children
-  } = props;
-  return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, header && /*#__PURE__*/external_React_default().createElement(DSSubHeader, null, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
-    message: header
-  })), /*#__PURE__*/external_React_default().createElement("div", {
-    className: `ds-card-grid ${className}`
   }, children));
 }
 function CardGrid_IntersectionObserver({
   children,
   windowObj = window,
   onIntersecting
 }) {
   const intersectionElement = (0,external_React_namespaceObject.useRef)(null);
@@ -8206,24 +8195,30 @@ function CardGrid_IntersectionObserver({
       return (_observer = observer) === null || _observer === void 0 ? void 0 : _observer.disconnect();
     };
   }, [windowObj, onIntersecting]);
   return /*#__PURE__*/external_React_default().createElement("div", {
     ref: intersectionElement
   }, children);
 }
 function RecentSavesContainer({
-  className,
+  gridClassName = "",
   dispatch,
   windowObj = window,
-  items = 3
+  items = 3,
+  source = "CARDGRID_RECENT_SAVES"
 }) {
   const {
     recentSavesData,
-    isUserLoggedIn
+    isUserLoggedIn,
+    experimentData: {
+      utmCampaign,
+      utmContent,
+      utmSource
+    }
   } = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.DiscoveryStream);
   const [visible, setVisible] = (0,external_React_namespaceObject.useState)(false);
   const onIntersecting = (0,external_React_namespaceObject.useCallback)(() => setVisible(true), []);
   (0,external_React_namespaceObject.useEffect)(() => {
     if (visible) {
       dispatch(actionCreators.AlsoToMain({
         type: actionTypes.DISCOVERY_STREAM_POCKET_STATE_INIT
       }));
@@ -8243,30 +8238,43 @@ function RecentSavesContainer({
     return null;
   }
 
   function renderCard(rec, index) {
     return /*#__PURE__*/external_React_default().createElement(DSCard, {
       key: `dscard-${(rec === null || rec === void 0 ? void 0 : rec.id) || index}`,
       id: rec.id,
       pos: index,
-      type: "CARDGRID_RECENT_SAVES",
+      type: source,
       image_src: rec.image_src,
       raw_image_src: rec.raw_image_src,
       word_count: rec.word_count,
       time_to_read: rec.time_to_read,
       title: rec.title,
       excerpt: rec.excerpt,
       url: rec.url,
       source: rec.domain,
       isRecentSave: true,
       dispatch: dispatch
     });
   }
 
+  function onMyListClicked() {
+    dispatch(actionCreators.UserEvent({
+      event: "CLICK",
+      source: `${source}_VIEW_LIST`
+    }));
+  }
+
+  let queryParams = `?utm_source=${utmSource}`;
+
+  if (utmCampaign && utmContent) {
+    queryParams += `&utm_content=${utmContent}&utm_campaign=${utmCampaign}`;
+  }
+
   const recentSavesCards = []; // We fill the cards with a for loop over an inline map because
   // we want empty placeholders if there are not enough cards.
 
   for (let index = 0; index < items; index++) {
     const recentSave = recentSavesData[index];
 
     if (!recentSave) {
       recentSavesCards.push( /*#__PURE__*/external_React_default().createElement(PlaceholderDSCard, {
@@ -8285,20 +8293,29 @@ function RecentSavesContainer({
         url: recentSave.resolved_url,
         domain: (_recentSave$domain_me = recentSave.domain_metadata) === null || _recentSave$domain_me === void 0 ? void 0 : _recentSave$domain_me.name,
         excerpt: recentSave.excerpt
       }, index));
     }
   } // We are visible and logged in.
 
 
-  return /*#__PURE__*/external_React_default().createElement(GridContainer, {
-    className: className,
-    header: "Recently Saved to your List"
-  }, recentSavesCards);
+  return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement(DSSubHeader, null, /*#__PURE__*/external_React_default().createElement("span", {
+    className: "section-title"
+  }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+    message: "Recently Saved to your List"
+  })), /*#__PURE__*/external_React_default().createElement(SafeAnchor, {
+    onLinkClick: onMyListClicked,
+    className: "section-sub-link",
+    url: `https://getpocket.com/a${queryParams}`
+  }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+    message: "View My List"
+  }))), /*#__PURE__*/external_React_default().createElement("div", {
+    className: gridClassName
+  }, recentSavesCards));
 }
 class _CardGrid extends (external_React_default()).PureComponent {
   constructor(props) {
     super(props);
     this.state = {
       moreLoaded: false
     };
     this.loadMoreClicked = this.loadMoreClicked.bind(this);
@@ -8473,29 +8490,35 @@ class _CardGrid extends (external_React_
 
 
     const variantClass = this.props.display_variant ? `ds-card-grid-${this.props.display_variant}` : ``;
     const hideCardBackgroundClass = hideCardBackground ? `ds-card-grid-hide-background` : ``;
     const fourCardLayoutClass = fourCardLayout ? `ds-card-grid-four-card-variant` : ``;
     const hideDescriptionsClassName = !hideDescriptions ? `ds-card-grid-include-descriptions` : ``;
     const compactGridClassName = compactGrid ? `ds-card-grid-compact` : ``;
     const hybridLayoutClassName = hybridLayout ? `ds-card-grid-hybrid-layout` : ``;
-    const className = `ds-card-grid-${this.props.border} ${variantClass} ${hybridLayoutClassName} ${hideCardBackgroundClass} ${fourCardLayoutClass} ${hideDescriptionsClassName} ${compactGridClassName}`;
-    return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, ((_essentialReadsCards = essentialReadsCards) === null || _essentialReadsCards === void 0 ? void 0 : _essentialReadsCards.length) > 0 && /*#__PURE__*/external_React_default().createElement(GridContainer, {
-      className: className
+    const gridClassName = `ds-card-grid ds-card-grid-${this.props.border} ${variantClass} ${hybridLayoutClassName} ${hideCardBackgroundClass} ${fourCardLayoutClass} ${hideDescriptionsClassName} ${compactGridClassName}`;
+    return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, ((_essentialReadsCards = essentialReadsCards) === null || _essentialReadsCards === void 0 ? void 0 : _essentialReadsCards.length) > 0 && /*#__PURE__*/external_React_default().createElement("div", {
+      className: gridClassName
     }, essentialReadsCards), showRecentSaves && /*#__PURE__*/external_React_default().createElement(RecentSavesContainer, {
-      className: className,
+      gridClassName: gridClassName,
       dispatch: this.props.dispatch
-    }), ((_editorsPicksCards = editorsPicksCards) === null || _editorsPicksCards === void 0 ? void 0 : _editorsPicksCards.length) > 0 && /*#__PURE__*/external_React_default().createElement(GridContainer, {
-      className: className,
-      header: "Editor\u2019s Picks"
-    }, editorsPicksCards), (cards === null || cards === void 0 ? void 0 : cards.length) > 0 && /*#__PURE__*/external_React_default().createElement(GridContainer, {
-      className: className,
-      header: moreRecsHeader
-    }, cards));
+    }), ((_editorsPicksCards = editorsPicksCards) === null || _editorsPicksCards === void 0 ? void 0 : _editorsPicksCards.length) > 0 && /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement(DSSubHeader, null, /*#__PURE__*/external_React_default().createElement("span", {
+      className: "section-title"
+    }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+      message: "Editor\u2019s Picks"
+    }))), /*#__PURE__*/external_React_default().createElement("div", {
+      className: gridClassName
+    }, editorsPicksCards)), (cards === null || cards === void 0 ? void 0 : cards.length) > 0 && /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, moreRecsHeader && /*#__PURE__*/external_React_default().createElement(DSSubHeader, null, /*#__PURE__*/external_React_default().createElement("span", {
+      className: "section-title"
+    }, /*#__PURE__*/external_React_default().createElement(FluentOrText, {
+      message: moreRecsHeader
+    }))), /*#__PURE__*/external_React_default().createElement("div", {
+      className: gridClassName
+    }, cards)));
   }
 
   render() {
     const {
       data
     } = this.props; // Handle a render before feed has been fetched by displaying nothing
 
     if (!data) {
--- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx
@@ -1,14 +1,13 @@
 import {
   _CardGrid as CardGrid,
   IntersectionObserver,
   RecentSavesContainer,
   DSSubHeader,
-  GridContainer,
 } from "content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid";
 import { combineReducers, createStore } from "redux";
 import { INITIAL_STATE, reducers } from "common/Reducers.jsm";
 import { Provider } from "react-redux";
 import {
   DSCard,
   PlaceholderDSCard,
   LastCardMessage,
@@ -40,20 +39,20 @@ describe("<CardGrid>", () => {
   it("should render an empty div", () => {
     assert.ok(wrapper.exists());
     assert.lengthOf(wrapper.children(), 0);
   });
 
   it("should render DSCards", () => {
     wrapper.setProps({ items: 2, data: { recommendations: [{}, {}] } });
 
-    assert.lengthOf(wrapper.find(GridContainer).children(), 2);
+    assert.lengthOf(wrapper.find(".ds-card-grid").children(), 2);
     assert.equal(
       wrapper
-        .find(GridContainer)
+        .find(".ds-card-grid")
         .children()
         .at(0)
         .type(),
       DSCard
     );
   });
 
   it("should add hero classname to card grid", () => {
@@ -250,53 +249,16 @@ describe("<IntersectionObserver>", () =>
 describe("<RecentSavesContainer>", () => {
   let wrapper;
   let fakeWindow;
   let intersectEntries;
   let dispatch;
 
   beforeEach(() => {
     dispatch = sinon.stub();
-    intersectEntries = [{ isIntersecting: false }];
-    fakeWindow = {
-      IntersectionObserver: buildIntersectionObserver(intersectEntries),
-    };
-    wrapper = mount(
-      <WrapWithProvider>
-        <RecentSavesContainer windowObj={fakeWindow} dispatch={dispatch} />
-      </WrapWithProvider>
-    ).find(RecentSavesContainer);
-  });
-
-  it("should render an IntersectionObserver when not visible", () => {
-    assert.ok(wrapper.exists());
-    assert.ok(wrapper.find(IntersectionObserver).exists());
-  });
-
-  it("should render a nothing if visible until we log in", () => {
-    intersectEntries = [{ isIntersecting: true }];
-    fakeWindow = {
-      IntersectionObserver: buildIntersectionObserver(intersectEntries),
-    };
-    wrapper = mount(
-      <WrapWithProvider>
-        <RecentSavesContainer windowObj={fakeWindow} dispatch={dispatch} />
-      </WrapWithProvider>
-    ).find(RecentSavesContainer);
-    assert.ok(!wrapper.find(IntersectionObserver).exists());
-    assert.calledOnce(dispatch);
-    assert.calledWith(
-      dispatch,
-      ac.AlsoToMain({
-        type: at.DISCOVERY_STREAM_POCKET_STATE_INIT,
-      })
-    );
-  });
-
-  it("should render a GridContainer if visible and logged in", () => {
     intersectEntries = [{ isIntersecting: true }];
     fakeWindow = {
       IntersectionObserver: buildIntersectionObserver(intersectEntries),
     };
     wrapper = mount(
       <WrapWithProvider
         state={{
           DiscoveryStream: {
@@ -306,19 +268,82 @@ describe("<RecentSavesContainer>", () =>
                 resolved_id: "resolved_id",
                 top_image_url: "top_image_url",
                 title: "title",
                 resolved_url: "resolved_url",
                 domain: "domain",
                 excerpt: "excerpt",
               },
             ],
+            experimentData: {
+              utmSource: "utmSource",
+              utmContent: "utmContent",
+              utmCampaign: "utmCampaign",
+            },
           },
         }}
       >
+        <RecentSavesContainer
+          gridClassName="ds-card-grid"
+          windowObj={fakeWindow}
+          dispatch={dispatch}
+        />
+      </WrapWithProvider>
+    ).find(RecentSavesContainer);
+  });
+
+  it("should render an IntersectionObserver when not visible", () => {
+    intersectEntries = [{ isIntersecting: false }];
+    fakeWindow = {
+      IntersectionObserver: buildIntersectionObserver(intersectEntries),
+    };
+    wrapper = mount(
+      <WrapWithProvider>
         <RecentSavesContainer windowObj={fakeWindow} dispatch={dispatch} />
       </WrapWithProvider>
     ).find(RecentSavesContainer);
-    assert.lengthOf(wrapper.find(GridContainer), 1);
+
+    assert.ok(wrapper.exists());
+    assert.ok(wrapper.find(IntersectionObserver).exists());
+  });
+
+  it("should render nothing if visible until we log in", () => {
+    assert.ok(!wrapper.find(IntersectionObserver).exists());
+    assert.calledOnce(dispatch);
+    assert.calledWith(
+      dispatch,
+      ac.AlsoToMain({
+        type: at.DISCOVERY_STREAM_POCKET_STATE_INIT,
+      })
+    );
+  });
+
+  it("should render a grid if visible and logged in", () => {
+    assert.lengthOf(wrapper.find(".ds-card-grid"), 1);
+    assert.lengthOf(wrapper.find(DSSubHeader), 1);
     assert.lengthOf(wrapper.find(PlaceholderDSCard), 2);
     assert.lengthOf(wrapper.find(DSCard), 3);
   });
+
+  it("should render a my list link with proper utm params", () => {
+    assert.equal(
+      wrapper
+        .find(".section-sub-link")
+        .at(0)
+        .prop("url"),
+      "https://getpocket.com/a?utm_source=utmSource&utm_content=utmContent&utm_campaign=utmCampaign"
+    );
+  });
+
+  it("should fire a UserEvent for my list clicks", () => {
+    wrapper
+      .find(".section-sub-link")
+      .at(0)
+      .simulate("click");
+    assert.calledWith(
+      dispatch,
+      ac.UserEvent({
+        event: "CLICK",
+        source: `CARDGRID_RECENT_SAVES_VIEW_LIST`,
+      })
+    );
+  });
 });