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