blob: 8a676595022d162e0bbdc13fcdff614a6e8c2f4f [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <EarlGrey/EarlGrey.h>
#import <XCTest/XCTest.h>
#include <memory>
#include <vector>
#include "base/bind.h"
#include "base/ios/ios_util.h"
#include "base/mac/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#include "base/test/scoped_command_line.h"
#include "components/keyed_service/ios/browser_state_keyed_service_factory.h"
#include "components/ntp_snippets/content_suggestion.h"
#include "components/ntp_snippets/content_suggestions_service.h"
#include "components/ntp_snippets/mock_content_suggestions_provider.h"
#include "components/strings/grit/components_strings.h"
#include "components/unified_consent/feature.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/chrome_switches.h"
#include "ios/chrome/browser/ntp_snippets/ios_chrome_content_suggestions_service_factory.h"
#include "ios/chrome/browser/ntp_snippets/ios_chrome_content_suggestions_service_factory_util.h"
#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_header_item.h"
#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_learn_more_item.h"
#include "ios/chrome/browser/ui/content_suggestions/content_suggestions_collection_utils.h"
#import "ios/chrome/browser/ui/content_suggestions/ntp_home_constant.h"
#import "ios/chrome/browser/ui/content_suggestions/ntp_home_provider_test_singleton.h"
#import "ios/chrome/browser/ui/content_suggestions/ntp_home_test_utils.h"
#include "ios/chrome/browser/ui/util/ui_util.h"
#import "ios/chrome/browser/ui/util/uikit_ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/app/chrome_test_util.h"
#import "ios/chrome/test/app/tab_test_util.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey_ui.h"
#import "ios/chrome/test/earl_grey/chrome_error_util.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/strings/grit/ui_strings.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using namespace ntp_snippets;
using testing::_;
using testing::Invoke;
using testing::WithArg;
namespace {
const char kPageLoadedString[] = "Page loaded!";
const char kPageURL[] = "/test-page.html";
const char kPageTitle[] = "Page title!";
// Scrolls the collection view in order to have the toolbar menu icon visible.
void ScrollUp() {
[[[EarlGrey
selectElementWithMatcher:grey_allOf(chrome_test_util::ToolsMenuButton(),
grey_sufficientlyVisible(), nil)]
usingSearchAction:grey_scrollInDirection(kGREYDirectionUp, 150)
onElementWithMatcher:chrome_test_util::ContentSuggestionCollectionView()]
assertWithMatcher:grey_notNil()];
}
// Provides responses for redirect and changed window location URLs.
std::unique_ptr<net::test_server::HttpResponse> StandardResponse(
const net::test_server::HttpRequest& request) {
if (request.relative_url != kPageURL) {
return nullptr;
}
std::unique_ptr<net::test_server::BasicHttpResponse> http_response =
std::make_unique<net::test_server::BasicHttpResponse>();
http_response->set_code(net::HTTP_OK);
http_response->set_content("<html><head><title>" + std::string(kPageTitle) +
"</title></head><body>" +
std::string(kPageLoadedString) + "</body></html>");
return std::move(http_response);
}
// Returns a suggestion created from the |category|, |suggestion_id| and the
// |url|.
ContentSuggestion Suggestion(Category category,
std::string suggestion_id,
GURL url) {
ContentSuggestion suggestion(category, suggestion_id, url);
suggestion.set_title(base::UTF8ToUTF16(url.spec()));
return suggestion;
}
// Select the cell with the |matcher| by scrolling the collection.
// 200 is a reasonable scroll displacement that works for all UI elements, while
// not being too slow.
GREYElementInteraction* CellWithMatcher(id<GREYMatcher> matcher) {
// Start the scroll from the middle of the screen in case the bottom of the
// screen is obscured by the bottom toolbar.
id<GREYAction> action =
grey_scrollInDirectionWithStartPoint(kGREYDirectionDown, 230, 0.5, 0.5);
return [[EarlGrey
selectElementWithMatcher:grey_allOf(matcher, grey_sufficientlyVisible(),
nil)]
usingSearchAction:action
onElementWithMatcher:chrome_test_util::ContentSuggestionCollectionView()];
}
} // namespace
#pragma mark - TestCase
// Test case for the ContentSuggestion UI.
@interface ContentSuggestionsTestCase : ChromeTestCase
// Current non-incognito browser state.
@property(nonatomic, assign, readonly) ios::ChromeBrowserState* browserState;
// Mock provider from the singleton.
@property(nonatomic, assign, readonly) MockContentSuggestionsProvider* provider;
// Article category, used by the singleton.
@property(nonatomic, assign, readonly) Category category;
@end
@implementation ContentSuggestionsTestCase
#pragma mark - Setup/Teardown
+ (void)setUp {
[super setUp];
[self closeAllTabs];
ios::ChromeBrowserState* browserState =
chrome_test_util::GetOriginalBrowserState();
// Sets the ContentSuggestionsService associated with this browserState to a
// service with no provider registered, allowing to register fake providers
// which do not require internet connection. The previous service is deleted.
IOSChromeContentSuggestionsServiceFactory::GetInstance()->SetTestingFactory(
browserState,
base::BindRepeating(&CreateChromeContentSuggestionsService));
ContentSuggestionsService* service =
IOSChromeContentSuggestionsServiceFactory::GetForBrowserState(
browserState);
[[ContentSuggestionsTestSingleton sharedInstance]
registerArticleProvider:service];
}
+ (void)tearDown {
[self closeAllTabs];
ios::ChromeBrowserState* browserState =
chrome_test_util::GetOriginalBrowserState();
// Resets the Service associated with this browserState to a new service with
// no providers. The previous service is deleted.
IOSChromeContentSuggestionsServiceFactory::GetInstance()->SetTestingFactory(
browserState,
base::BindRepeating(&CreateChromeContentSuggestionsService));
[super tearDown];
}
// Per crbug.com/845186, Disable flakey iPad Retina tests that are limited
// to iOS 10.2.
+ (NSArray*)testInvocations {
#if TARGET_IPHONE_SIMULATOR
if (IsIPadIdiom() && !base::ios::IsRunningOnOrLater(10, 3, 0))
return @[];
#endif // TARGET_IPHONE_SIMULATOR
return [super testInvocations];
}
- (void)setUp {
self.provider->FireCategoryStatusChanged(self.category,
CategoryStatus::AVAILABLE);
[super setUp];
}
- (void)tearDown {
self.provider->FireCategoryStatusChanged(
self.category, CategoryStatus::ALL_SUGGESTIONS_EXPLICITLY_DISABLED);
[ChromeEarlGrey clearBrowsingHistory];
[super tearDown];
}
#pragma mark - Tests
// Tests that the additional items (when more is pressed) are kept when
// switching tabs.
- (void)testAdditionalItemsKept {
// Set server up.
self.testServer->RegisterRequestHandler(base::Bind(&StandardResponse));
GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
const GURL pageURL = self.testServer->GetURL(kPageURL);
// Add 3 suggestions, persisted accross page loads.
std::vector<ContentSuggestion> suggestions;
suggestions.emplace_back(
Suggestion(self.category, "chromium1", GURL("http://chromium.org/1")));
suggestions.emplace_back(
Suggestion(self.category, "chromium2", GURL("http://chromium.org/2")));
suggestions.emplace_back(
Suggestion(self.category, "chromium3", GURL("http://chromium.org/3")));
self.provider->FireSuggestionsChanged(self.category, std::move(suggestions));
// Set up the action when "More" is tapped.
AdditionalSuggestionsHelper helper(pageURL);
EXPECT_CALL(*self.provider, FetchMock(_, _, _))
.WillOnce(WithArg<2>(Invoke(
&helper, &AdditionalSuggestionsHelper::SendAdditionalSuggestions)));
// Tap on more, which adds 10 elements.
[CellWithMatcher(chrome_test_util::ButtonWithAccessibilityLabelId(
IDS_IOS_CONTENT_SUGGESTIONS_FOOTER_TITLE)) performAction:grey_tap()];
// Make sure some items are loaded.
[CellWithMatcher(grey_accessibilityID(@"AdditionalSuggestion2"))
assertWithMatcher:grey_notNil()];
// Open a new Tab.
ScrollUp();
[ChromeEarlGreyUI openNewTab];
CHROME_EG_ASSERT_NO_ERROR([ChromeEarlGrey waitForMainTabCount:2]);
// Go back to the previous tab.
chrome_test_util::SelectTabAtIndexInCurrentMode(0);
// Make sure the additional items are still displayed.
[CellWithMatcher(grey_accessibilityID(@"AdditionalSuggestion2"))
assertWithMatcher:grey_notNil()];
}
// Tests that a switch for the ContentSuggestions exists in the settings. The
// behavior depends on having a real remote provider, so it cannot be tested
// here.
- (void)testPrivacySwitch {
if (unified_consent::IsUnifiedConsentFeatureEnabled()) {
EARL_GREY_TEST_DISABLED(
@"Privacy swich for ContentSuggestion was moved to the Sync and Google "
"services settings screen, so it is no longer present in the privacy "
"section. This test is now covered by "
"-[GoogleServicesSettingsTestCase testOpeningServices].");
}
[ChromeEarlGreyUI openSettingsMenu];
[ChromeEarlGreyUI
tapSettingsMenuButton:chrome_test_util::SettingsMenuPrivacyButton()];
[[EarlGrey selectElementWithMatcher:
chrome_test_util::StaticTextWithAccessibilityLabelId(
IDS_IOS_OPTIONS_SEARCH_URL_SUGGESTIONS)]
assertWithMatcher:grey_sufficientlyVisible()];
}
// Tests that when tapping a suggestion, it is opened. When going back, the
// disposition of the collection takes into account the previous scroll, even
// when more is tapped.
- (void)testOpenPageAndGoBackWithMoreContent {
// Set server up.
self.testServer->RegisterRequestHandler(base::Bind(&StandardResponse));
GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
const GURL pageURL = self.testServer->GetURL(kPageURL);
// Add 3 suggestions, persisted accross page loads.
std::vector<ContentSuggestion> suggestions;
suggestions.emplace_back(
Suggestion(self.category, "chromium1", GURL("http://chromium.org/1")));
suggestions.emplace_back(
Suggestion(self.category, "chromium2", GURL("http://chromium.org/2")));
suggestions.emplace_back(
Suggestion(self.category, "chromium3", GURL("http://chromium.org/3")));
self.provider->FireSuggestionsChanged(self.category, std::move(suggestions));
// Set up the action when "More" is tapped.
AdditionalSuggestionsHelper helper(pageURL);
EXPECT_CALL(*self.provider, FetchMock(_, _, _))
.WillOnce(WithArg<2>(Invoke(
&helper, &AdditionalSuggestionsHelper::SendAdditionalSuggestions)));
// Tap on more, which adds 10 elements.
[CellWithMatcher(chrome_test_util::ButtonWithAccessibilityLabelId(
IDS_IOS_CONTENT_SUGGESTIONS_FOOTER_TITLE)) performAction:grey_tap()];
// Make sure to scroll to the bottom.
[CellWithMatcher(grey_accessibilityID(
[ContentSuggestionsLearnMoreItem accessibilityIdentifier]))
assertWithMatcher:grey_notNil()];
// Open the last item.
[CellWithMatcher(grey_accessibilityID(@"AdditionalSuggestion9"))
performAction:grey_tap()];
// Check that the page has been opened.
CHROME_EG_ASSERT_NO_ERROR(
[ChromeEarlGrey waitForWebStateContainingText:kPageLoadedString]);
[[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(
pageURL.GetContent())]
assertWithMatcher:grey_notNil()];
CHROME_EG_ASSERT_NO_ERROR([ChromeEarlGrey waitForMainTabCount:1]);
CHROME_EG_ASSERT_NO_ERROR([ChromeEarlGrey waitForIncognitoTabCount:0]);
// Go back.
[[EarlGrey selectElementWithMatcher:chrome_test_util::BackButton()]
performAction:grey_tap()];
// Check that the first items are visible as the collection should be
// scrolled.
[[EarlGrey
selectElementWithMatcher:grey_accessibilityID(@"http://chromium.org/3")]
assertWithMatcher:grey_sufficientlyVisible()];
}
// Tests that the "Learn More" cell is present only if there is a suggestion in
// the section.
- (void)testLearnMore {
id<GREYAction> action =
grey_scrollInDirectionWithStartPoint(kGREYDirectionDown, 200, 0.5, 0.5);
[[[EarlGrey selectElementWithMatcher:grey_accessibilityID(
[ContentSuggestionsLearnMoreItem
accessibilityIdentifier])]
usingSearchAction:action
onElementWithMatcher:chrome_test_util::ContentSuggestionCollectionView()]
assertWithMatcher:grey_nil()];
std::vector<ContentSuggestion> suggestions;
suggestions.emplace_back(
Suggestion(self.category, "chromium", GURL("http://chromium.org")));
self.provider->FireSuggestionsChanged(self.category, std::move(suggestions));
[CellWithMatcher(grey_accessibilityID(
[ContentSuggestionsLearnMoreItem accessibilityIdentifier]))
assertWithMatcher:grey_sufficientlyVisible()];
}
// Tests the "Open in New Tab" action of the Most Visited context menu.
- (void)testMostVisitedNewTab {
[self setupMostVisitedTileLongPress];
const GURL pageURL = self.testServer->GetURL(kPageURL);
// Open in new tab.
[[EarlGrey
selectElementWithMatcher:chrome_test_util::ButtonWithAccessibilityLabelId(
IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWTAB)]
performAction:grey_tap()];
// Check a new page in normal model is opened.
CHROME_EG_ASSERT_NO_ERROR([ChromeEarlGrey waitForMainTabCount:2]);
CHROME_EG_ASSERT_NO_ERROR([ChromeEarlGrey waitForIncognitoTabCount:0]);
// Check that the tab has been opened in background.
ConditionBlock condition = ^{
NSError* error = nil;
[[EarlGrey selectElementWithMatcher:chrome_test_util::
ContentSuggestionCollectionView()]
assertWithMatcher:grey_notNil()
error:&error];
return error == nil;
};
GREYAssert(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForUIElementTimeout, condition),
@"Collection view not visible");
// Check the page has been correctly opened.
chrome_test_util::SelectTabAtIndexInCurrentMode(1);
CHROME_EG_ASSERT_NO_ERROR(
[ChromeEarlGrey waitForWebStateContainingText:kPageLoadedString]);
[[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(
pageURL.GetContent())]
assertWithMatcher:grey_notNil()];
}
// Tests the "Open in New Incognito Tab" action of the Most Visited context
// menu.
- (void)testMostVisitedNewIncognitoTab {
[self setupMostVisitedTileLongPress];
const GURL pageURL = self.testServer->GetURL(kPageURL);
// Open in new incognito tab.
[[EarlGrey selectElementWithMatcher:
chrome_test_util::ButtonWithAccessibilityLabelId(
IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWINCOGNITOTAB)]
performAction:grey_tap()];
CHROME_EG_ASSERT_NO_ERROR([ChromeEarlGrey waitForMainTabCount:1]);
CHROME_EG_ASSERT_NO_ERROR([ChromeEarlGrey waitForIncognitoTabCount:1]);
// Check that the tab has been opened in foreground.
CHROME_EG_ASSERT_NO_ERROR(
[ChromeEarlGrey waitForWebStateContainingText:kPageLoadedString]);
[[EarlGrey selectElementWithMatcher:chrome_test_util::OmniboxText(
pageURL.GetContent())]
assertWithMatcher:grey_notNil()];
GREYAssertTrue(chrome_test_util::IsIncognitoMode(),
@"Test did not switch to incognito");
}
// action.
- (void)testMostVisitedRemoveUndo {
[self setupMostVisitedTileLongPress];
const GURL pageURL = self.testServer->GetURL(kPageURL);
NSString* pageTitle = base::SysUTF8ToNSString(kPageTitle);
// Tap on remove.
[[EarlGrey
selectElementWithMatcher:chrome_test_util::ButtonWithAccessibilityLabelId(
IDS_IOS_CONTENT_SUGGESTIONS_REMOVE)]
performAction:grey_tap()];
// Check the tile is removed.
[[EarlGrey
selectElementWithMatcher:
grey_allOf(
chrome_test_util::StaticTextWithAccessibilityLabel(pageTitle),
grey_sufficientlyVisible(), nil)] assertWithMatcher:grey_nil()];
// Check the snack bar notifying the user that an element has been removed is
// displayed.
[[EarlGrey
selectElementWithMatcher:chrome_test_util::ButtonWithAccessibilityLabelId(
IDS_IOS_NEW_TAB_MOST_VISITED_ITEM_REMOVED)]
assertWithMatcher:grey_sufficientlyVisible()];
// Tap on undo.
[[EarlGrey
selectElementWithMatcher:chrome_test_util::ButtonWithAccessibilityLabelId(
IDS_NEW_TAB_UNDO_THUMBNAIL_REMOVE)]
performAction:grey_tap()];
// Check the tile is back.
ConditionBlock condition = ^{
NSError* error = nil;
[[EarlGrey
selectElementWithMatcher:
grey_allOf(
chrome_test_util::StaticTextWithAccessibilityLabel(pageTitle),
grey_sufficientlyVisible(), nil)]
assertWithMatcher:grey_notNil()
error:&error];
return error == nil;
};
NSString* errorMessage =
@"The tile wasn't added back after hitting 'Undo' on the snackbar";
GREYAssert(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForUIElementTimeout, condition),
errorMessage);
[[EarlGrey selectElementWithMatcher:
grey_allOf(chrome_test_util::StaticTextWithAccessibilityLabel(
pageTitle),
grey_sufficientlyVisible(), nil)]
assertWithMatcher:grey_notNil()];
}
// Tests that the context menu has the correct actions.
- (void)testMostVisitedLongPress {
[self setupMostVisitedTileLongPress];
if (!IsRegularXRegularSizeClass()) {
[[EarlGrey selectElementWithMatcher:
chrome_test_util::ButtonWithAccessibilityLabelId(
IDS_APP_CANCEL)] assertWithMatcher:grey_interactable()];
}
// No read later.
[[EarlGrey
selectElementWithMatcher:chrome_test_util::ButtonWithAccessibilityLabelId(
IDS_IOS_CONTENT_CONTEXT_ADDTOREADINGLIST)]
assertWithMatcher:grey_nil()];
}
#pragma mark - Properties
- (ios::ChromeBrowserState*)browserState {
return chrome_test_util::GetOriginalBrowserState();
}
- (MockContentSuggestionsProvider*)provider {
return [[ContentSuggestionsTestSingleton sharedInstance] provider];
}
- (Category)category {
return Category::FromKnownCategory(KnownCategories::ARTICLES);
}
#pragma mark - Test utils
// Setup a most visited tile, and open the context menu by long pressing on it.
- (void)setupMostVisitedTileLongPress {
self.testServer->RegisterRequestHandler(base::Bind(&StandardResponse));
GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
const GURL pageURL = self.testServer->GetURL(kPageURL);
NSString* pageTitle = base::SysUTF8ToNSString(kPageTitle);
// Clear history and verify that the tile does not exist.
[ChromeEarlGrey clearBrowsingHistory];
CHROME_EG_ASSERT_NO_ERROR([ChromeEarlGrey loadURL:pageURL]);
CHROME_EG_ASSERT_NO_ERROR(
[ChromeEarlGrey waitForWebStateContainingText:kPageLoadedString]);
// After loading URL, need to do another action before opening a new tab
// with the icon present.
CHROME_EG_ASSERT_NO_ERROR([ChromeEarlGrey goBack]);
[[self class] closeAllTabs];
CHROME_EG_ASSERT_NO_ERROR([ChromeEarlGrey openNewTab]);
[[EarlGrey selectElementWithMatcher:
chrome_test_util::StaticTextWithAccessibilityLabel(pageTitle)]
performAction:grey_longPress()];
}
@end