1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.feed.v2;
6 
7 import static com.google.common.truth.Truth.assertThat;
8 
9 import static org.junit.Assert.assertEquals;
10 import static org.junit.Assert.assertNotNull;
11 import static org.junit.Assert.assertTrue;
12 import static org.mockito.Mockito.any;
13 import static org.mockito.Mockito.anyBoolean;
14 import static org.mockito.Mockito.anyInt;
15 import static org.mockito.Mockito.anyLong;
16 import static org.mockito.Mockito.eq;
17 import static org.mockito.Mockito.never;
18 import static org.mockito.Mockito.verify;
19 import static org.mockito.Mockito.when;
20 
21 import android.app.Activity;
22 import android.support.test.filters.SmallTest;
23 import android.view.View;
24 import android.widget.FrameLayout;
25 import android.widget.LinearLayout;
26 import android.widget.TextView;
27 
28 import androidx.recyclerview.widget.RecyclerView;
29 
30 import com.google.protobuf.ByteString;
31 
32 import org.junit.After;
33 import org.junit.Assert;
34 import org.junit.Before;
35 import org.junit.Rule;
36 import org.junit.Test;
37 import org.junit.rules.TestRule;
38 import org.junit.runner.RunWith;
39 import org.mockito.ArgumentCaptor;
40 import org.mockito.ArgumentMatchers;
41 import org.mockito.Captor;
42 import org.mockito.InOrder;
43 import org.mockito.Mock;
44 import org.mockito.Mockito;
45 import org.mockito.MockitoAnnotations;
46 import org.robolectric.Robolectric;
47 import org.robolectric.annotation.Config;
48 import org.robolectric.shadows.ShadowLog;
49 
50 import org.chromium.base.Callback;
51 import org.chromium.base.metrics.test.ShadowRecordHistogram;
52 import org.chromium.base.task.test.ShadowPostTask;
53 import org.chromium.base.test.BaseRobolectricTestRunner;
54 import org.chromium.base.test.util.JniMocker;
55 import org.chromium.base.test.util.MetricsUtils.HistogramDelta;
56 import org.chromium.chrome.browser.AppHooks;
57 import org.chromium.chrome.browser.AppHooksImpl;
58 import org.chromium.chrome.browser.feed.shared.stream.Stream.ContentChangedListener;
59 import org.chromium.chrome.browser.feedback.HelpAndFeedbackLauncherImpl;
60 import org.chromium.chrome.browser.native_page.NativePageNavigationDelegate;
61 import org.chromium.chrome.browser.ntp.NewTabPageUma;
62 import org.chromium.chrome.browser.profiles.Profile;
63 import org.chromium.chrome.browser.tab.MockTab;
64 import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
65 import org.chromium.chrome.browser.xsurface.FeedActionsHandler;
66 import org.chromium.chrome.browser.xsurface.ProcessScope;
67 import org.chromium.chrome.test.util.browser.Features;
68 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
69 import org.chromium.components.feed.proto.FeedUiProto.Slice;
70 import org.chromium.components.feed.proto.FeedUiProto.StreamUpdate;
71 import org.chromium.components.feed.proto.FeedUiProto.StreamUpdate.SliceUpdate;
72 import org.chromium.components.feed.proto.FeedUiProto.XSurfaceSlice;
73 import org.chromium.ui.mojom.WindowOpenDisposition;
74 
75 import java.util.Arrays;
76 import java.util.HashMap;
77 import java.util.Map;
78 import java.util.concurrent.TimeUnit;
79 
80 /** Unit tests for {@link FeedStreamSeSurface}. */
81 @RunWith(BaseRobolectricTestRunner.class)
82 @Config(manifest = Config.NONE, shadows = {ShadowPostTask.class, ShadowRecordHistogram.class})
83 public class FeedStreamSurfaceTest {
84     class ContentChangeWatcher implements ContentChangedListener {
85         @Override
onContentChanged()86         public void onContentChanged() {
87             mContentChanged = true;
88         }
89 
90         @Override
onAddStarting()91         public void onAddStarting() {}
92 
93         @Override
onAddFinished()94         public void onAddFinished() {}
95     }
96 
97     private static final String TEST_DATA = "test";
98     private static final String TEST_URL = "https://www.chromium.org";
99     private static final int LOAD_MORE_TRIGGER_LOOKAHEAD = 5;
100     private FeedStreamSurface mFeedStreamSurface;
101     private Activity mActivity;
102     private RecyclerView mRecyclerView;
103     private LinearLayout mParent;
104     private FakeLinearLayoutManager mLayoutManager;
105     private FeedListContentManager mContentManager;
106     private boolean mContentChanged;
107 
108     @Mock
109     private SnackbarManager mSnackbarManager;
110     @Mock
111     private FeedActionsHandler.SnackbarController mSnackbarController;
112     @Mock
113     private BottomSheetController mBottomSheetController;
114     @Mock
115     private NativePageNavigationDelegate mPageNavigationDelegate;
116     @Mock
117     private HelpAndFeedbackLauncherImpl mHelpAndFeedbackLauncherImpl;
118     @Mock
119     Profile mProfileMock;
120     @Mock
121     private FeedServiceBridge.Natives mFeedServiceBridgeJniMock;
122     @Mock
123     private FeedStreamSurface.ShareHelperWrapper mShareHelper;
124 
125     @Captor
126     private ArgumentCaptor<Map<String, String>> mMapCaptor;
127 
128     @Rule
129     public JniMocker mocker = new JniMocker();
130     // Enable the Features class, so we can call code which checks to see if features are enabled
131     // without crashing.
132     @Rule
133     public TestRule mFeaturesProcessorRule = new Features.JUnitProcessor();
134 
135     @Mock
136     private FeedStreamSurface.Natives mFeedStreamSurfaceJniMock;
137 
138     @Mock
139     private AppHooksImpl mAppHooks;
140     @Mock
141     private ProcessScope mProcessScope;
142 
143     @Before
setUp()144     public void setUp() throws Exception {
145         MockitoAnnotations.initMocks(this);
146         mActivity = Robolectric.buildActivity(Activity.class).setup().get();
147         mParent = new LinearLayout(mActivity);
148         mocker.mock(FeedStreamSurfaceJni.TEST_HOOKS, mFeedStreamSurfaceJniMock);
149         mocker.mock(FeedServiceBridgeJni.TEST_HOOKS, mFeedServiceBridgeJniMock);
150 
151         when(mFeedServiceBridgeJniMock.getLoadMoreTriggerLookahead())
152                 .thenReturn(LOAD_MORE_TRIGGER_LOOKAHEAD);
153 
154         when(mAppHooks.getExternalSurfaceProcessScope(any())).thenReturn(mProcessScope);
155 
156         AppHooks.setInstanceForTesting(mAppHooks);
157 
158         Profile.setLastUsedProfileForTesting(mProfileMock);
159         mFeedStreamSurface = Mockito.spy(new FeedStreamSurface(mActivity, false, mSnackbarManager,
160                 mPageNavigationDelegate, mBottomSheetController, mHelpAndFeedbackLauncherImpl,
161                 /* isPlaceholderShown= */ false, mShareHelper));
162         mContentManager = mFeedStreamSurface.getFeedListContentManagerForTesting();
163         mFeedStreamSurface.mRootView = Mockito.spy(mFeedStreamSurface.mRootView);
164         mRecyclerView = mFeedStreamSurface.mRootView;
165         mLayoutManager = new FakeLinearLayoutManager(mActivity);
166         mRecyclerView.setLayoutManager(mLayoutManager);
167         mFeedStreamSurface.addContentChangedListener(new ContentChangeWatcher());
168 
169         // Since we use a mockito spy, we need to replace the entry in sSurfaces.
170         FeedStreamSurface.sSurfaces.clear();
171         FeedStreamSurface.sSurfaces.add(mFeedStreamSurface);
172 
173         // Print logs to stdout.
174         ShadowLog.stream = System.out;
175     }
176 
177     @After
tearDown()178     public void tearDown() {
179         mFeedStreamSurface.destroy();
180         FeedStreamSurface.shutdownForTesting();
181         AppHooks.setInstanceForTesting(null);
182     }
183 
184     @Test
185     @SmallTest
testContentChangedOnStreamUpdated()186     public void testContentChangedOnStreamUpdated() {
187         startupAndSetVisible();
188 
189         // Add 1 slice.
190         StreamUpdate update = StreamUpdate.newBuilder()
191                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("a"))
192                                       .build();
193         mContentChanged = false;
194         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
195         assertTrue(mContentChanged);
196         assertEquals(1, mContentManager.getItemCount());
197 
198         // Remove 1 slice.
199         update = StreamUpdate.newBuilder().build();
200         mContentChanged = false;
201         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
202         assertTrue(mContentChanged);
203         assertEquals(0, mContentManager.getItemCount());
204     }
205 
206     @Test
207     @SmallTest
testContentChangedOnSetHeaderViews()208     public void testContentChangedOnSetHeaderViews() {
209         startupAndSetVisible();
210 
211         mContentChanged = false;
212         mFeedStreamSurface.setHeaderViews(Arrays.asList(new View(mActivity)));
213         assertTrue(mContentChanged);
214         assertEquals(1, mContentManager.getItemCount());
215     }
216 
217     @Test
218     @SmallTest
testAddSlicesOnStreamUpdated()219     public void testAddSlicesOnStreamUpdated() {
220         startupAndSetVisible();
221         // Add 3 new slices at first.
222         StreamUpdate update = StreamUpdate.newBuilder()
223                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("a"))
224                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("b"))
225                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("c"))
226                                       .build();
227         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
228         assertEquals(3, mContentManager.getItemCount());
229         assertEquals(0, mContentManager.findContentPositionByKey("a"));
230         assertEquals(1, mContentManager.findContentPositionByKey("b"));
231         assertEquals(2, mContentManager.findContentPositionByKey("c"));
232 
233         // Add 2 more slices.
234         update = StreamUpdate.newBuilder()
235                          .addUpdatedSlices(createSliceUpdateForExistingSlice("a"))
236                          .addUpdatedSlices(createSliceUpdateForExistingSlice("b"))
237                          .addUpdatedSlices(createSliceUpdateForExistingSlice("c"))
238                          .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("d"))
239                          .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("e"))
240                          .build();
241         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
242         assertEquals(5, mContentManager.getItemCount());
243         assertEquals(0, mContentManager.findContentPositionByKey("a"));
244         assertEquals(1, mContentManager.findContentPositionByKey("b"));
245         assertEquals(2, mContentManager.findContentPositionByKey("c"));
246         assertEquals(3, mContentManager.findContentPositionByKey("d"));
247         assertEquals(4, mContentManager.findContentPositionByKey("e"));
248     }
249 
250     @Test
251     @SmallTest
testAddNewSlicesWithSameIds()252     public void testAddNewSlicesWithSameIds() {
253         startupAndSetVisible();
254         // Add 2 new slices at first.
255         StreamUpdate update = StreamUpdate.newBuilder()
256                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("a"))
257                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("b"))
258                                       .build();
259         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
260         assertEquals(2, mContentManager.getItemCount());
261         assertEquals(0, mContentManager.findContentPositionByKey("a"));
262         assertEquals(1, mContentManager.findContentPositionByKey("b"));
263 
264         // Add 2 new slice with same ids as before.
265         update = StreamUpdate.newBuilder()
266                          .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("b"))
267                          .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("a"))
268                          .build();
269         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
270         assertEquals(2, mContentManager.getItemCount());
271         assertEquals(0, mContentManager.findContentPositionByKey("b"));
272         assertEquals(1, mContentManager.findContentPositionByKey("a"));
273     }
274 
275     @Test
276     @SmallTest
testRemoveSlicesOnStreamUpdated()277     public void testRemoveSlicesOnStreamUpdated() {
278         startupAndSetVisible();
279         // Add 3 new slices at first.
280         StreamUpdate update = StreamUpdate.newBuilder()
281                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("a"))
282                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("b"))
283                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("c"))
284                                       .build();
285         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
286         assertEquals(3, mContentManager.getItemCount());
287         assertEquals(0, mContentManager.findContentPositionByKey("a"));
288         assertEquals(1, mContentManager.findContentPositionByKey("b"));
289         assertEquals(2, mContentManager.findContentPositionByKey("c"));
290 
291         // Remove 1 slice.
292         update = StreamUpdate.newBuilder()
293                          .addUpdatedSlices(createSliceUpdateForExistingSlice("a"))
294                          .addUpdatedSlices(createSliceUpdateForExistingSlice("c"))
295                          .build();
296         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
297         assertEquals(2, mContentManager.getItemCount());
298         assertEquals(0, mContentManager.findContentPositionByKey("a"));
299         assertEquals(1, mContentManager.findContentPositionByKey("c"));
300 
301         // Remove 2 slices.
302         update = StreamUpdate.newBuilder().build();
303         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
304         assertEquals(0, mContentManager.getItemCount());
305     }
306 
307     @Test
308     @SmallTest
testReorderSlicesOnStreamUpdated()309     public void testReorderSlicesOnStreamUpdated() {
310         startupAndSetVisible();
311         // Add 3 new slices at first.
312         StreamUpdate update = StreamUpdate.newBuilder()
313                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("a"))
314                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("b"))
315                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("c"))
316                                       .build();
317         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
318         assertEquals(3, mContentManager.getItemCount());
319         assertEquals(0, mContentManager.findContentPositionByKey("a"));
320         assertEquals(1, mContentManager.findContentPositionByKey("b"));
321         assertEquals(2, mContentManager.findContentPositionByKey("c"));
322 
323         // Reorder 1 slice.
324         update = StreamUpdate.newBuilder()
325                          .addUpdatedSlices(createSliceUpdateForExistingSlice("c"))
326                          .addUpdatedSlices(createSliceUpdateForExistingSlice("a"))
327                          .addUpdatedSlices(createSliceUpdateForExistingSlice("b"))
328                          .build();
329         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
330         assertEquals(3, mContentManager.getItemCount());
331         assertEquals(0, mContentManager.findContentPositionByKey("c"));
332         assertEquals(1, mContentManager.findContentPositionByKey("a"));
333         assertEquals(2, mContentManager.findContentPositionByKey("b"));
334 
335         // Reorder 2 slices.
336         update = StreamUpdate.newBuilder()
337                          .addUpdatedSlices(createSliceUpdateForExistingSlice("a"))
338                          .addUpdatedSlices(createSliceUpdateForExistingSlice("b"))
339                          .addUpdatedSlices(createSliceUpdateForExistingSlice("c"))
340                          .build();
341         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
342         assertEquals(3, mContentManager.getItemCount());
343         assertEquals(0, mContentManager.findContentPositionByKey("a"));
344         assertEquals(1, mContentManager.findContentPositionByKey("b"));
345         assertEquals(2, mContentManager.findContentPositionByKey("c"));
346     }
347 
348     @Test
349     @SmallTest
testComplexOperationsOnStreamUpdated()350     public void testComplexOperationsOnStreamUpdated() {
351         startupAndSetVisible();
352         // Add 3 new slices at first.
353         StreamUpdate update = StreamUpdate.newBuilder()
354                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("a"))
355                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("b"))
356                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("c"))
357                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("d"))
358                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("e"))
359                                       .build();
360         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
361         assertEquals(5, mContentManager.getItemCount());
362         assertEquals(0, mContentManager.findContentPositionByKey("a"));
363         assertEquals(1, mContentManager.findContentPositionByKey("b"));
364         assertEquals(2, mContentManager.findContentPositionByKey("c"));
365         assertEquals(3, mContentManager.findContentPositionByKey("d"));
366         assertEquals(4, mContentManager.findContentPositionByKey("e"));
367 
368         // Combo of add, remove and reorder operations.
369         update = StreamUpdate.newBuilder()
370                          .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("f"))
371                          .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("g"))
372                          .addUpdatedSlices(createSliceUpdateForExistingSlice("a"))
373                          .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("h"))
374                          .addUpdatedSlices(createSliceUpdateForExistingSlice("c"))
375                          .addUpdatedSlices(createSliceUpdateForExistingSlice("e"))
376                          .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("i"))
377                          .build();
378         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
379         assertEquals(7, mContentManager.getItemCount());
380         assertEquals(0, mContentManager.findContentPositionByKey("f"));
381         assertEquals(1, mContentManager.findContentPositionByKey("g"));
382         assertEquals(2, mContentManager.findContentPositionByKey("a"));
383         assertEquals(3, mContentManager.findContentPositionByKey("h"));
384         assertEquals(4, mContentManager.findContentPositionByKey("c"));
385         assertEquals(5, mContentManager.findContentPositionByKey("e"));
386         assertEquals(6, mContentManager.findContentPositionByKey("i"));
387     }
388 
389     @Test
390     @SmallTest
testAddHeaderViews()391     public void testAddHeaderViews() {
392         startupAndSetVisible();
393         View v0 = new View(mActivity);
394         View v1 = new View(mActivity);
395 
396         mFeedStreamSurface.setHeaderViews(Arrays.asList(v0, v1));
397         assertEquals(2, mContentManager.getItemCount());
398         assertEquals(v0, getNativeView(0));
399         assertEquals(v1, getNativeView(1));
400     }
401 
402     @Test
403     @SmallTest
testUpdateHeaderViews()404     public void testUpdateHeaderViews() {
405         startupAndSetVisible();
406         View v0 = new View(mActivity);
407         View v1 = new View(mActivity);
408 
409         mFeedStreamSurface.setHeaderViews(Arrays.asList(v0, v1));
410         assertEquals(2, mContentManager.getItemCount());
411         assertEquals(v0, getNativeView(0));
412         assertEquals(v1, getNativeView(1));
413 
414         View v2 = new View(mActivity);
415         View v3 = new View(mActivity);
416 
417         mFeedStreamSurface.setHeaderViews(Arrays.asList(v2, v0, v3));
418         assertEquals(3, mContentManager.getItemCount());
419         assertEquals(v2, getNativeView(0));
420         assertEquals(v0, getNativeView(1));
421         assertEquals(v3, getNativeView(2));
422     }
423 
424     @Test
425     @SmallTest
testComplexOperationsOnStreamUpdatedAfterSetHeaderViews()426     public void testComplexOperationsOnStreamUpdatedAfterSetHeaderViews() {
427         startupAndSetVisible();
428         // Set 2 header views first. These should always be there throughout stream update.
429         View v0 = new View(mActivity);
430         View v1 = new View(mActivity);
431         mFeedStreamSurface.setHeaderViews(Arrays.asList(v0, v1));
432         assertEquals(2, mContentManager.getItemCount());
433         assertEquals(v0, getNativeView(0));
434         assertEquals(v1, getNativeView(1));
435         final int headers = 2;
436 
437         // Add 3 new slices at first.
438         StreamUpdate update = StreamUpdate.newBuilder()
439                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("a"))
440                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("b"))
441                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("c"))
442                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("d"))
443                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("e"))
444                                       .build();
445         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
446         assertEquals(headers + 5, mContentManager.getItemCount());
447         assertEquals(headers + 0, mContentManager.findContentPositionByKey("a"));
448         assertEquals(headers + 1, mContentManager.findContentPositionByKey("b"));
449         assertEquals(headers + 2, mContentManager.findContentPositionByKey("c"));
450         assertEquals(headers + 3, mContentManager.findContentPositionByKey("d"));
451         assertEquals(headers + 4, mContentManager.findContentPositionByKey("e"));
452 
453         // Combo of add, remove and reorder operations.
454         update = StreamUpdate.newBuilder()
455                          .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("f"))
456                          .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("g"))
457                          .addUpdatedSlices(createSliceUpdateForExistingSlice("a"))
458                          .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("h"))
459                          .addUpdatedSlices(createSliceUpdateForExistingSlice("c"))
460                          .addUpdatedSlices(createSliceUpdateForExistingSlice("e"))
461                          .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("i"))
462                          .build();
463         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
464         assertEquals(headers + 7, mContentManager.getItemCount());
465         assertEquals(headers + 0, mContentManager.findContentPositionByKey("f"));
466         assertEquals(headers + 1, mContentManager.findContentPositionByKey("g"));
467         assertEquals(headers + 2, mContentManager.findContentPositionByKey("a"));
468         assertEquals(headers + 3, mContentManager.findContentPositionByKey("h"));
469         assertEquals(headers + 4, mContentManager.findContentPositionByKey("c"));
470         assertEquals(headers + 5, mContentManager.findContentPositionByKey("e"));
471         assertEquals(headers + 6, mContentManager.findContentPositionByKey("i"));
472     }
473 
474     @Test
475     @SmallTest
testNavigateTab()476     public void testNavigateTab() {
477         HistogramDelta actionOpenedSnippetDelta = new HistogramDelta(
478                 "NewTabPage.ActionAndroid2", NewTabPageUma.ACTION_OPENED_SNIPPET);
479         when(mPageNavigationDelegate.openUrl(anyInt(), any())).thenReturn(new MockTab(1, false));
480         mFeedStreamSurface.navigateTab(TEST_URL, null);
481         verify(mPageNavigationDelegate)
482                 .openUrl(ArgumentMatchers.eq(WindowOpenDisposition.CURRENT_TAB), any());
483 
484         assertEquals(1, actionOpenedSnippetDelta.getDelta());
485     }
486 
487     @Test
488     @SmallTest
testNavigateNewTab()489     public void testNavigateNewTab() {
490         HistogramDelta actionOpenedSnippetDelta = new HistogramDelta(
491                 "NewTabPage.ActionAndroid2", NewTabPageUma.ACTION_OPENED_SNIPPET);
492         when(mPageNavigationDelegate.openUrl(anyInt(), any())).thenReturn(new MockTab(1, false));
493         mFeedStreamSurface.navigateNewTab(TEST_URL);
494         verify(mPageNavigationDelegate)
495                 .openUrl(ArgumentMatchers.eq(WindowOpenDisposition.NEW_BACKGROUND_TAB), any());
496         assertEquals(1, actionOpenedSnippetDelta.getDelta());
497     }
498 
499     @Test
500     @SmallTest
testNavigateIncognitoTab()501     public void testNavigateIncognitoTab() {
502         HistogramDelta actionOpenedSnippetDelta = new HistogramDelta(
503                 "NewTabPage.ActionAndroid2", NewTabPageUma.ACTION_OPENED_SNIPPET);
504         when(mPageNavigationDelegate.openUrl(anyInt(), any())).thenReturn(new MockTab(1, false));
505         mFeedStreamSurface.navigateIncognitoTab(TEST_URL);
506         verify(mPageNavigationDelegate)
507                 .openUrl(ArgumentMatchers.eq(WindowOpenDisposition.OFF_THE_RECORD), any());
508         assertEquals(1, actionOpenedSnippetDelta.getDelta());
509     }
510 
511     @Test
512     @SmallTest
testSendFeedback()513     public void testSendFeedback() {
514         final String testUrl = "https://www.chromium.org";
515         final String testTitle = "Chromium based browsers for the win!";
516         final String xSurfaceCardTitle = "Card Title";
517         final String cardTitle = "CardTitle";
518         final String cardUrl = "CardUrl";
519         // Arrange.
520         Map<String, String> productSpecificDataMap = new HashMap<>();
521         productSpecificDataMap.put(FeedStreamSurface.XSURFACE_CARD_URL, testUrl);
522         productSpecificDataMap.put(xSurfaceCardTitle, testTitle);
523 
524         // Act.
525         mFeedStreamSurface.sendFeedback(productSpecificDataMap);
526 
527         // Assert.
528         verify(mHelpAndFeedbackLauncherImpl)
529                 .showFeedback(any(), any(), eq(testUrl), eq(FeedStreamSurface.FEEDBACK_REPORT_TYPE),
530                         mMapCaptor.capture(), eq(FeedStreamSurface.FEEDBACK_CONTEXT));
531 
532         // Check that the map contents are as expected.
533         assertThat(mMapCaptor.getValue()).containsEntry(cardUrl, testUrl);
534         assertThat(mMapCaptor.getValue()).containsEntry(cardTitle, testTitle);
535     }
536 
537     @Test
538     @SmallTest
testShowSnackbar()539     public void testShowSnackbar() {
540         mFeedStreamSurface.showSnackbar(
541                 "message", "Undo", FeedActionsHandler.SnackbarDuration.SHORT, mSnackbarController);
542         verify(mSnackbarManager).showSnackbar(any());
543     }
544 
545     @Test
546     @SmallTest
testShowBottomSheet()547     public void testShowBottomSheet() {
548         mFeedStreamSurface.showBottomSheet(new TextView(mActivity));
549         verify(mBottomSheetController).requestShowContent(any(), anyBoolean());
550     }
551 
552     @Test
553     @SmallTest
testDismissBottomSheet()554     public void testDismissBottomSheet() {
555         mFeedStreamSurface.showBottomSheet(new TextView(mActivity));
556         mFeedStreamSurface.dismissBottomSheet();
557         verify(mBottomSheetController).hideContent(any(), anyBoolean());
558     }
559 
560     @Test
561     @SmallTest
testShare()562     public void testShare() {
563         String url = "http://www.foo.com";
564         String title = "fooTitle";
565         mFeedStreamSurface.share(url, title);
566         verify(mShareHelper).share(url, title);
567     }
568 
569     @Test
570     @SmallTest
testRemoveContentsOnSurfaceClosed()571     public void testRemoveContentsOnSurfaceClosed() {
572         startupAndSetVisible();
573 
574         // Set 2 header views first.
575         View v0 = new View(mActivity);
576         View v1 = new View(mActivity);
577         mFeedStreamSurface.setHeaderViews(Arrays.asList(v0, v1));
578         assertEquals(2, mContentManager.getItemCount());
579         assertEquals(v0, getNativeView(0));
580         assertEquals(v1, getNativeView(1));
581         final int headers = 2;
582 
583         // Add 3 new slices.
584         StreamUpdate update = StreamUpdate.newBuilder()
585                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("a"))
586                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("b"))
587                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("c"))
588                                       .build();
589         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
590         assertEquals(headers + 3, mContentManager.getItemCount());
591 
592         // Closing the surface should remove all non-header contents.
593         mContentChanged = false;
594         mFeedStreamSurface.setStreamVisibility(false);
595         assertTrue(mContentChanged);
596         assertEquals(headers, mContentManager.getItemCount());
597         assertEquals(v0, getNativeView(0));
598         assertEquals(v1, getNativeView(1));
599     }
600 
601     @Test
602     @SmallTest
testLoadMoreOnDismissal()603     public void testLoadMoreOnDismissal() {
604         startupAndSetVisible();
605         final int itemCount = 10;
606 
607         // loadMore not triggered due to last visible item not falling into lookahead range.
608         mLayoutManager.setLastVisiblePosition(itemCount - LOAD_MORE_TRIGGER_LOOKAHEAD - 1);
609         mLayoutManager.setItemCount(itemCount);
610         mFeedStreamSurface.commitDismissal(0);
611         verify(mFeedStreamSurfaceJniMock, never())
612                 .loadMore(anyLong(), any(FeedStreamSurface.class), any(Callback.class));
613 
614         // loadMore triggered.
615         mLayoutManager.setLastVisiblePosition(itemCount - LOAD_MORE_TRIGGER_LOOKAHEAD + 1);
616         mLayoutManager.setItemCount(itemCount);
617         mFeedStreamSurface.commitDismissal(0);
618         verify(mFeedStreamSurfaceJniMock)
619                 .loadMore(anyLong(), any(FeedStreamSurface.class), any(Callback.class));
620     }
621 
622     @Test
623     @SmallTest
testLoadMoreOnNavigateNewTab()624     public void testLoadMoreOnNavigateNewTab() {
625         startupAndSetVisible();
626         final int itemCount = 10;
627 
628         // loadMore not triggered due to last visible item not falling into lookahead range.
629         mLayoutManager.setLastVisiblePosition(itemCount - LOAD_MORE_TRIGGER_LOOKAHEAD - 1);
630         mLayoutManager.setItemCount(itemCount);
631         mFeedStreamSurface.navigateNewTab("");
632         verify(mFeedStreamSurfaceJniMock, never())
633                 .loadMore(anyLong(), any(FeedStreamSurface.class), any(Callback.class));
634 
635         // loadMore triggered.
636         mLayoutManager.setLastVisiblePosition(itemCount - LOAD_MORE_TRIGGER_LOOKAHEAD + 1);
637         mLayoutManager.setItemCount(itemCount);
638         mFeedStreamSurface.navigateNewTab("");
639         verify(mFeedStreamSurfaceJniMock)
640                 .loadMore(anyLong(), any(FeedStreamSurface.class), any(Callback.class));
641     }
642 
643     @Test
644     @SmallTest
testLoadMoreOnNavigateIncognitoTab()645     public void testLoadMoreOnNavigateIncognitoTab() {
646         startupAndSetVisible();
647         final int itemCount = 10;
648 
649         // loadMore not triggered due to last visible item not falling into lookahead range.
650         mLayoutManager.setLastVisiblePosition(itemCount - LOAD_MORE_TRIGGER_LOOKAHEAD - 1);
651         mLayoutManager.setItemCount(itemCount);
652         mFeedStreamSurface.navigateIncognitoTab("");
653         verify(mFeedStreamSurfaceJniMock, never())
654                 .loadMore(anyLong(), any(FeedStreamSurface.class), any(Callback.class));
655 
656         // loadMore triggered.
657         mLayoutManager.setLastVisiblePosition(itemCount - LOAD_MORE_TRIGGER_LOOKAHEAD + 1);
658         mLayoutManager.setItemCount(itemCount);
659         mFeedStreamSurface.navigateIncognitoTab("");
660         verify(mFeedStreamSurfaceJniMock)
661                 .loadMore(anyLong(), any(FeedStreamSurface.class), any(Callback.class));
662     }
663 
664     @Test
665     @SmallTest
testSurfaceOpenedAndClosed()666     public void testSurfaceOpenedAndClosed() {
667         // The surface won't open until after startup().
668         mFeedStreamSurface.setStreamVisibility(true);
669         mFeedStreamSurface.setStreamContentVisibility(true);
670         Assert.assertFalse(mFeedStreamSurface.isOpened());
671 
672         // Exercise turning off stream visibility while hidden.
673         mFeedStreamSurface.setStreamVisibility(false);
674         Assert.assertFalse(mFeedStreamSurface.isOpened());
675 
676         // Trigger open.
677         mFeedStreamSurface.setStreamVisibility(true);
678         FeedStreamSurface.startup();
679         Assert.assertTrue(mFeedStreamSurface.isOpened());
680 
681         mFeedStreamSurface.setStreamVisibility(false);
682         Assert.assertFalse(mFeedStreamSurface.isOpened());
683 
684         mFeedStreamSurface.setStreamVisibility(true);
685         Assert.assertTrue(mFeedStreamSurface.isOpened());
686 
687         mFeedStreamSurface.setStreamContentVisibility(false);
688         Assert.assertFalse(mFeedStreamSurface.isOpened());
689 
690         mFeedStreamSurface.setStreamContentVisibility(true);
691         Assert.assertTrue(mFeedStreamSurface.isOpened());
692     }
693 
694     @Test
695     @SmallTest
testClearAll()696     public void testClearAll() {
697         InOrder order = Mockito.inOrder(mFeedStreamSurfaceJniMock, mProcessScope);
698         startupAndSetVisible();
699         order.verify(mFeedStreamSurfaceJniMock)
700                 .surfaceOpened(anyLong(), any(FeedStreamSurface.class));
701 
702         FeedStreamSurface.clearAll();
703         order.verify(mFeedStreamSurfaceJniMock)
704                 .surfaceClosed(anyLong(), any(FeedStreamSurface.class));
705         order.verify(mProcessScope).resetAccount();
706         order.verify(mFeedStreamSurfaceJniMock)
707                 .surfaceOpened(anyLong(), any(FeedStreamSurface.class));
708     }
709 
710     @Test
711     @SmallTest
testFindChildViewContainingDescendentNullParameters()712     public void testFindChildViewContainingDescendentNullParameters() {
713         startupAndSetVisible();
714         View v = new View(mActivity);
715         assertEquals(null, mFeedStreamSurface.findChildViewContainingDescendent(null, v));
716         assertEquals(null, mFeedStreamSurface.findChildViewContainingDescendent(v, null));
717     }
718 
719     @Test
720     @SmallTest
testFindChildViewContainingDescendentNotADescendent()721     public void testFindChildViewContainingDescendentNotADescendent() {
722         startupAndSetVisible();
723         View v1 = new View(mActivity);
724         LinearLayout v2 = new LinearLayout(mActivity);
725         View v2Child = new View(mActivity);
726         v2.addView(v2Child);
727 
728         assertEquals(null, mFeedStreamSurface.findChildViewContainingDescendent(v1, v2));
729         assertEquals(null, mFeedStreamSurface.findChildViewContainingDescendent(v1, v2Child));
730     }
731 
732     @Test
733     @SmallTest
testFindChildViewContainingDescendentDirectDescendent()734     public void testFindChildViewContainingDescendentDirectDescendent() {
735         startupAndSetVisible();
736         LinearLayout parent = new LinearLayout(mActivity);
737         View child = new View(mActivity);
738         parent.addView(child);
739 
740         assertEquals(child, mFeedStreamSurface.findChildViewContainingDescendent(parent, child));
741     }
742 
743     @Test
744     @SmallTest
testFindChildViewContainingDescendentIndirectDescendent()745     public void testFindChildViewContainingDescendentIndirectDescendent() {
746         startupAndSetVisible();
747         LinearLayout parent = new LinearLayout(mActivity);
748         LinearLayout child = new LinearLayout(mActivity);
749         View grandChild = new View(mActivity);
750         parent.addView(child);
751         child.addView(grandChild);
752 
753         assertEquals(
754                 child, mFeedStreamSurface.findChildViewContainingDescendent(parent, grandChild));
755     }
756 
757     @Test
758     @SmallTest
testOnStreamUpdatedIgnoredWhenNotOpen()759     public void testOnStreamUpdatedIgnoredWhenNotOpen() {
760         // Surface not opened initially.
761         StreamUpdate update = StreamUpdate.newBuilder()
762                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("a"))
763                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("b"))
764                                       .build();
765         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
766 
767         assertEquals(0, mContentManager.getItemCount());
768     }
769 
770     @Test
771     @SmallTest
testNavigateReportsCorrectSlice()772     public void testNavigateReportsCorrectSlice() {
773         startupAndSetVisible();
774         StreamUpdate update = StreamUpdate.newBuilder()
775                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("a"))
776                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("b"))
777                                       .build();
778         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
779 
780         View childA = new View(mActivity);
781         mRecyclerView.addView(childA);
782         View childB = new View(mActivity);
783         mRecyclerView.addView(childB);
784 
785         // findChildViewContainingDescendent() won't work on its own because mRecyclerView is a
786         // mockito spy, and therefore child.getParent() != mRecyclerView.
787         Mockito.doReturn(childA)
788                 .when(mFeedStreamSurface)
789                 .findChildViewContainingDescendent(mRecyclerView, childA);
790         Mockito.doReturn(childB)
791                 .when(mFeedStreamSurface)
792                 .findChildViewContainingDescendent(mRecyclerView, childB);
793         Mockito.doReturn(0).when(mRecyclerView).getChildAdapterPosition(childA);
794         Mockito.doReturn(1).when(mRecyclerView).getChildAdapterPosition(childB);
795 
796         mFeedStreamSurface.navigateTab("http://someurl", childB);
797         mFeedStreamSurface.navigateNewTab("http://someurl", childA);
798 
799         verify(mFeedStreamSurfaceJniMock)
800                 .reportOpenAction(anyLong(), any(FeedStreamSurface.class), eq("b"));
801         verify(mFeedStreamSurfaceJniMock)
802                 .reportOpenInNewTabAction(anyLong(), any(FeedStreamSurface.class), eq("a"));
803     }
804 
805     @Test
806     @SmallTest
testNavigateFromBottomSheetReportsCorrectSlice()807     public void testNavigateFromBottomSheetReportsCorrectSlice() {
808         startupAndSetVisible();
809 
810         StreamUpdate update = StreamUpdate.newBuilder()
811                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("a"))
812                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("b"))
813                                       .build();
814         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
815 
816         View childA = new View(mActivity);
817         mRecyclerView.addView(childA);
818         View childB = new View(mActivity);
819         mRecyclerView.addView(childB);
820         LinearLayout bottomSheetView = new LinearLayout(mActivity);
821         View menuItem = new View(mActivity);
822         bottomSheetView.addView(menuItem);
823 
824         // findChildViewContainingDescendent() won't work on its own because mRecyclerView is a
825         // mockito spy, and therefore child.getParent() != mRecyclerView.
826         Mockito.doReturn(childA)
827                 .when(mFeedStreamSurface)
828                 .findChildViewContainingDescendent(mRecyclerView, childA);
829         Mockito.doReturn(childB)
830                 .when(mFeedStreamSurface)
831                 .findChildViewContainingDescendent(mRecyclerView, childB);
832         Mockito.doReturn(0).when(mRecyclerView).getChildAdapterPosition(childA);
833         Mockito.doReturn(1).when(mRecyclerView).getChildAdapterPosition(childB);
834 
835         mFeedStreamSurface.showBottomSheet(bottomSheetView, childB);
836         mFeedStreamSurface.navigateTab("http://someurl", menuItem);
837         mFeedStreamSurface.dismissBottomSheet();
838         mFeedStreamSurface.navigateNewTab("http://someurl", menuItem);
839 
840         verify(mFeedStreamSurfaceJniMock)
841                 .reportOpenAction(anyLong(), any(FeedStreamSurface.class), eq("b"));
842         // Bottom sheet closed for this navigation, so slice cannot be found.
843         verify(mFeedStreamSurfaceJniMock)
844                 .reportOpenInNewTabAction(anyLong(), any(FeedStreamSurface.class), eq(""));
845     }
846 
847     @Test
848     @SmallTest
testNavigateNoSliceFound()849     public void testNavigateNoSliceFound() {
850         startupAndSetVisible();
851 
852         StreamUpdate update = StreamUpdate.newBuilder()
853                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("a"))
854                                       .addUpdatedSlices(createSliceUpdateForNewXSurfaceSlice("b"))
855                                       .build();
856         mFeedStreamSurface.onStreamUpdated(update.toByteArray());
857 
858         View nonConnectedView = new View(mActivity);
859 
860         // findChildViewContainingDescendent() won't work on its own because mRecyclerView is a
861         // mockito spy, and therefore child.getParent() != mRecyclerView.
862         Mockito.doReturn(null)
863                 .when(mFeedStreamSurface)
864                 .findChildViewContainingDescendent(mRecyclerView, nonConnectedView);
865 
866         mFeedStreamSurface.navigateTab("http://someurl", nonConnectedView);
867 
868         verify(mFeedStreamSurfaceJniMock)
869                 .reportOpenAction(anyLong(), any(FeedStreamSurface.class), eq(""));
870     }
871 
872     @Test
873     @SmallTest
testScrollIsReportedOnIdle()874     public void testScrollIsReportedOnIdle() {
875         startupAndSetVisible();
876 
877         mFeedStreamSurface.streamScrolled(0, 100);
878         Robolectric.getForegroundThreadScheduler().advanceBy(1000, TimeUnit.MILLISECONDS);
879 
880         verify(mFeedStreamSurfaceJniMock)
881                 .reportStreamScrollStart(anyLong(), any(FeedStreamSurface.class));
882         verify(mFeedStreamSurfaceJniMock)
883                 .reportStreamScrolled(anyLong(), any(FeedStreamSurface.class), eq(100));
884     }
885 
886     @Test
887     @SmallTest
testScrollIsReportedOnClose()888     public void testScrollIsReportedOnClose() {
889         startupAndSetVisible();
890 
891         mFeedStreamSurface.streamScrolled(0, 100);
892         mFeedStreamSurface.setStreamContentVisibility(false);
893 
894         verify(mFeedStreamSurfaceJniMock)
895                 .reportStreamScrollStart(anyLong(), any(FeedStreamSurface.class));
896         verify(mFeedStreamSurfaceJniMock)
897                 .reportStreamScrolled(anyLong(), any(FeedStreamSurface.class), eq(100));
898     }
899 
createSliceUpdateForExistingSlice(String sliceId)900     private SliceUpdate createSliceUpdateForExistingSlice(String sliceId) {
901         return SliceUpdate.newBuilder().setSliceId(sliceId).build();
902     }
903 
createSliceUpdateForNewXSurfaceSlice(String sliceId)904     private SliceUpdate createSliceUpdateForNewXSurfaceSlice(String sliceId) {
905         return SliceUpdate.newBuilder().setSlice(createXSurfaceSSlice(sliceId)).build();
906     }
907 
createXSurfaceSSlice(String sliceId)908     private Slice createXSurfaceSSlice(String sliceId) {
909         return Slice.newBuilder()
910                 .setSliceId(sliceId)
911                 .setXsurfaceSlice(XSurfaceSlice.newBuilder()
912                                           .setXsurfaceFrame(ByteString.copyFromUtf8(TEST_DATA))
913                                           .build())
914                 .build();
915     }
916 
getNativeView(int index)917     private View getNativeView(int index) {
918         View view = ((FeedListContentManager.NativeViewContent) mContentManager.getContent(index))
919                             .getNativeView(mParent);
920         assertNotNull(view);
921         assertTrue(view instanceof FrameLayout);
922         return ((FrameLayout) view).getChildAt(0);
923     }
924 
startupAndSetVisible()925     void startupAndSetVisible() {
926         FeedStreamSurface.startup();
927         mFeedStreamSurface.setStreamContentVisibility(true);
928         mFeedStreamSurface.setStreamVisibility(true);
929     }
930 }
931