// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_tile_saver.h"
#import "base/run_loop.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/task_environment.h"
#import "components/ntp_tiles/ntp_tile.h"
#import "ios/chrome/browser/favicon/ui_bundled/favicon_attributes_provider.h"
#import "ios/chrome/common/app_group/app_group_constants.h"
#import "ios/chrome/common/ntp_tile/ntp_tile.h"
#import "ios/chrome/common/ui/favicon/favicon_attributes.h"
#import "ios/chrome/test/block_cleanup_test.h"
#import "net/base/apple/url_conversions.h"
#import "testing/gtest_mac.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "ui/base/test/ios/ui_image_test_utils.h"
namespace {
class ContentSuggestionsTileSaverControllerTest : public BlockCleanupTest {
protected:
void SetUp() override {
BlockCleanupTest::SetUp();
if ([[NSFileManager defaultManager]
fileExistsAtPath:[TestFaviconDirectory() path]]) {
[[NSFileManager defaultManager] removeItemAtURL:TestFaviconDirectory()
error:nil];
}
CreateMockImage([UIColor blackColor]);
NSUserDefaults* sharedDefaults = app_group::GetGroupUserDefaults();
[sharedDefaults removeObjectForKey:app_group::kSuggestedItems];
}
void TearDown() override {
if ([[NSFileManager defaultManager]
fileExistsAtPath:[TestFaviconDirectory() path]]) {
[[NSFileManager defaultManager] removeItemAtURL:TestFaviconDirectory()
error:nil];
}
NSUserDefaults* sharedDefaults = app_group::GetGroupUserDefaults();
[sharedDefaults removeObjectForKey:app_group::kSuggestedItems];
BlockCleanupTest::TearDown();
}
UIImage* CreateMockImage(UIColor* color) {
mock_image_ = ui::test::uiimage_utils::UIImageWithSizeAndSolidColor(
CGSizeMake(10, 10), color);
if (@available(iOS 16.1, *)) {
// Save the image to disk and reload it.
NSData* image_data = UIImagePNGRepresentation(mock_image_);
NSURL* mock_image_url = [NSFileManager.defaultManager.temporaryDirectory
URLByAppendingPathComponent:@"mock_image"];
[image_data writeToURL:mock_image_url atomically:YES];
mock_image_ = [UIImage imageWithContentsOfFile:mock_image_url.path];
}
return mock_image_;
}
NSURL* TestFaviconDirectory() {
return
[[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask]
firstObject] URLByAppendingPathComponent:@"testFaviconDirectory"];
}
void SetupMockCallback(id mock,
std::set<GURL> image_urls,
std::set<GURL> fallback_urls) {
OCMStub([[mock ignoringNonObjectArgs]
fetchFaviconAttributesForURL:GURL()
completion:[OCMArg isNotNil]])
.andDo(^(NSInvocation* invocation) {
GURL* urltest;
[invocation getArgument:&urltest atIndex:2];
if (image_urls.find(GURL(*urltest)) != image_urls.end()) {
__unsafe_unretained void (^callback)(id);
[invocation getArgument:&callback atIndex:3];
callback([FaviconAttributes attributesWithImage:mock_image_]);
} else if (fallback_urls.find(GURL(*urltest)) !=
fallback_urls.end()) {
__unsafe_unretained void (^callback)(id);
[invocation getArgument:&callback atIndex:3];
callback([FaviconAttributes
attributesWithMonogram:@"C"
textColor:UIColor.whiteColor
backgroundColor:UIColor.blueColor
defaultBackgroundColor:NO]);
}
});
}
// Checks that `tile` has an image and no fallback data.
// Checks that `tile` title and url match `expected_title` and
// `expected_url`.
// Checks that the file pointed by `tile` match `mock_image_`.
void VerifyWithImage(NTPTile* tile,
NSString* expected_title,
NSURL* expected_url) {
EXPECT_NSNE(tile, nil);
EXPECT_NSEQ(tile.title, expected_title);
EXPECT_NSEQ(tile.URL, expected_url);
EXPECT_NSNE(tile.faviconFileName, nil);
EXPECT_NSEQ(tile.fallbackTextColor, nil);
EXPECT_NSEQ(tile.fallbackBackgroundColor, nil);
EXPECT_TRUE([[NSFileManager defaultManager]
fileExistsAtPath:[[TestFaviconDirectory()
URLByAppendingPathComponent:tile.faviconFileName]
path]]);
UIImage* stored_image = [UIImage
imageWithContentsOfFile:
[[TestFaviconDirectory()
URLByAppendingPathComponent:tile.faviconFileName] path]];
EXPECT_NSEQ(UIImagePNGRepresentation(stored_image),
UIImagePNGRepresentation(mock_image_));
}
// Checks that `tile` has fallback data.
// Checks that `tile` title and url match `expected_title` and
// `expected_url`.
// Checks that the file pointed by `tile` does not exist.
void VerifyWithFallback(NTPTile* tile,
NSString* expected_title,
NSURL* expected_url) {
EXPECT_NSNE(tile, nil);
EXPECT_NSEQ(tile.title, expected_title);
EXPECT_NSEQ(tile.URL, expected_url);
EXPECT_NSNE(tile.faviconFileName, nil);
EXPECT_NSEQ(tile.fallbackTextColor, UIColor.whiteColor);
EXPECT_NSEQ(tile.fallbackBackgroundColor, UIColor.blueColor);
EXPECT_EQ(tile.fallbackIsDefaultColor, NO);
EXPECT_FALSE([[NSFileManager defaultManager]
fileExistsAtPath:[[TestFaviconDirectory()
URLByAppendingPathComponent:tile.faviconFileName]
path]]);
}
// Checks that `tile` has an image and fallback data.
// Checks that `tile` title and url match `expected_title` and
// `expected_url`.
// Checks that the file pointed by `tile` match `mock_image_`.
void VerifyWithFallbackAndImage(NTPTile* tile,
NSString* expected_title,
NSURL* expected_url) {
EXPECT_NSNE(tile, nil);
EXPECT_NSEQ(tile.title, expected_title);
EXPECT_NSEQ(tile.URL, expected_url);
EXPECT_NSNE(tile.faviconFileName, nil);
EXPECT_NSEQ(tile.fallbackTextColor, UIColor.whiteColor);
EXPECT_NSEQ(tile.fallbackBackgroundColor, UIColor.blueColor);
EXPECT_EQ(tile.fallbackIsDefaultColor, NO);
EXPECT_TRUE([[NSFileManager defaultManager]
fileExistsAtPath:[[TestFaviconDirectory()
URLByAppendingPathComponent:tile.faviconFileName]
path]]);
UIImage* stored_image = [UIImage
imageWithContentsOfFile:
[[TestFaviconDirectory()
URLByAppendingPathComponent:tile.faviconFileName] path]];
EXPECT_NSEQ(UIImagePNGRepresentation(stored_image),
UIImagePNGRepresentation(mock_image_));
}
protected:
base::test::TaskEnvironment scoped_task_evironment_;
UIImage* mock_image_;
};
TEST_F(ContentSuggestionsTileSaverControllerTest, SaveMostVisitedToDisk) {
ntp_tiles::NTPTile image_tile = ntp_tiles::NTPTile();
image_tile.title = u"Title";
image_tile.url = GURL("http://image.com");
ntp_tiles::NTPTile fallback_tile = ntp_tiles::NTPTile();
fallback_tile.title = u"Title";
fallback_tile.url = GURL("http://fallback.com");
id mock_favicon_fetcher = OCMClassMock([FaviconAttributesProvider class]);
SetupMockCallback(mock_favicon_fetcher, {image_tile.url},
{fallback_tile.url});
ntp_tiles::NTPTilesVector tiles = {
fallback_tile, // NTP tile with fallback data
image_tile, // NTP tile with favicon
};
content_suggestions_tile_saver::SaveMostVisitedToDisk(
tiles, mock_favicon_fetcher, TestFaviconDirectory());
// Wait for all asynchronous tasks to complete.
scoped_task_evironment_.RunUntilIdle();
// Read most visited from disk.
NSDictionary<NSURL*, NTPTile*>* saved_tiles =
content_suggestions_tile_saver::ReadSavedMostVisited();
EXPECT_EQ(saved_tiles.count, 2U);
NSString* fallback_title = base::SysUTF16ToNSString(fallback_tile.title);
NSURL* fallback_url = net::NSURLWithGURL(fallback_tile.url);
NTPTile* fallback_saved_tile = [saved_tiles objectForKey:fallback_url];
VerifyWithFallback(fallback_saved_tile, fallback_title, fallback_url);
NSString* image_title = base::SysUTF16ToNSString(image_tile.title);
NSURL* image_url = net::NSURLWithGURL(image_tile.url);
NTPTile* image_saved_tile = [saved_tiles objectForKey:image_url];
VerifyWithImage(image_saved_tile, image_title, image_url);
}
// TODO(crbug.com/40071361): reenable this test.
TEST_F(ContentSuggestionsTileSaverControllerTest, UpdateSingleFaviconFallback) {
// Set up test with 3 saved sites, 2 of which have a favicon.
ntp_tiles::NTPTile image_tile1 = ntp_tiles::NTPTile();
image_tile1.title = u"Title1";
image_tile1.url = GURL("http://image1.com");
ntp_tiles::NTPTile image_tile2 = ntp_tiles::NTPTile();
image_tile2.title = u"Title2";
image_tile2.url = GURL("http://image2.com");
ntp_tiles::NTPTile fallback_tile = ntp_tiles::NTPTile();
fallback_tile.title = u"Title";
fallback_tile.url = GURL("http://fallback.com");
id mock_favicon_fetcher = OCMClassMock([FaviconAttributesProvider class]);
SetupMockCallback(mock_favicon_fetcher, {image_tile1.url, image_tile2.url},
{fallback_tile.url});
ntp_tiles::NTPTilesVector tiles = {image_tile1, fallback_tile, image_tile2};
content_suggestions_tile_saver::SaveMostVisitedToDisk(
tiles, mock_favicon_fetcher, TestFaviconDirectory());
// Wait for all asynchronous tasks to complete.
scoped_task_evironment_.RunUntilIdle();
// Read most visited from disk.
NSDictionary<NSURL*, NTPTile*>* saved_tiles =
content_suggestions_tile_saver::ReadSavedMostVisited();
EXPECT_EQ(saved_tiles.count, 3U);
NSString* image_title1 = base::SysUTF16ToNSString(image_tile1.title);
NSURL* image_url1 = net::NSURLWithGURL(image_tile1.url);
NTPTile* image_saved_tile1 = [saved_tiles objectForKey:image_url1];
VerifyWithImage(image_saved_tile1, image_title1, image_url1);
NSString* image_title2 = base::SysUTF16ToNSString(image_tile2.title);
NSURL* image_url2 = net::NSURLWithGURL(image_tile2.url);
NTPTile* image_saved_tile2 = [saved_tiles objectForKey:image_url2];
VerifyWithImage(image_saved_tile2, image_title2, image_url2);
NSString* fallback_title = base::SysUTF16ToNSString(fallback_tile.title);
NSURL* fallback_url = net::NSURLWithGURL(fallback_tile.url);
NTPTile* fallback_saved_tile = [saved_tiles objectForKey:fallback_url];
VerifyWithFallback(fallback_saved_tile, fallback_title, fallback_url);
// Mock returning a fallback value for the first image tile.
id mock_favicon_fetcher2 = OCMClassMock([FaviconAttributesProvider class]);
SetupMockCallback(mock_favicon_fetcher2, {image_tile2.url},
{image_tile1.url, fallback_tile.url});
content_suggestions_tile_saver::UpdateSingleFavicon(
image_tile1.url, mock_favicon_fetcher2, TestFaviconDirectory());
// Wait for all asynchronous tasks to complete.
scoped_task_evironment_.RunUntilIdle();
// Read most visited from disk.
NSDictionary<NSURL*, NTPTile*>* saved_tiles_after_update =
content_suggestions_tile_saver::ReadSavedMostVisited();
EXPECT_EQ(saved_tiles_after_update.count, 3U);
// Verify that the first image tile now has callback data.
image_saved_tile1 = [saved_tiles_after_update objectForKey:image_url1];
VerifyWithFallback(image_saved_tile1, image_title1, image_url1);
// Verify that the other two tiles did not change.
image_saved_tile2 = [saved_tiles objectForKey:image_url2];
VerifyWithImage(image_saved_tile2, image_title2, image_url2);
fallback_saved_tile = [saved_tiles_after_update objectForKey:fallback_url];
VerifyWithFallback(fallback_saved_tile, fallback_title, fallback_url);
}
// Checks that the image saved for an item is deleted when the item is deleted.
TEST_F(ContentSuggestionsTileSaverControllerTest, DeleteOutdatedImage) {
ntp_tiles::NTPTile image_tile1 = ntp_tiles::NTPTile();
image_tile1.title = u"Title";
image_tile1.url = GURL("http://image1.com");
ntp_tiles::NTPTile image_tile2 = ntp_tiles::NTPTile();
image_tile2.title = u"Title";
image_tile2.url = GURL("http://image2.com");
id mock_favicon_fetcher = OCMClassMock([FaviconAttributesProvider class]);
SetupMockCallback(mock_favicon_fetcher, {image_tile1.url, image_tile2.url},
{});
ntp_tiles::NTPTilesVector tiles = {
image_tile1,
};
content_suggestions_tile_saver::SaveMostVisitedToDisk(
tiles, mock_favicon_fetcher, TestFaviconDirectory());
// Wait for all asynchronous tasks to complete.
scoped_task_evironment_.RunUntilIdle();
NSDictionary<NSURL*, NTPTile*>* saved_tiles =
content_suggestions_tile_saver::ReadSavedMostVisited();
NSString* image_title1 = base::SysUTF16ToNSString(image_tile1.title);
NSURL* image_url1 = net::NSURLWithGURL(image_tile1.url);
NTPTile* saved_tile1 = [saved_tiles objectForKey:image_url1];
VerifyWithImage(saved_tile1, image_title1, image_url1);
ntp_tiles::NTPTilesVector tiles2 = {
image_tile2,
};
content_suggestions_tile_saver::SaveMostVisitedToDisk(
tiles2, mock_favicon_fetcher, TestFaviconDirectory());
// Wait for all asynchronous tasks to complete.
scoped_task_evironment_.RunUntilIdle();
NSDictionary<NSURL*, NTPTile*>* saved_tiles2 =
content_suggestions_tile_saver::ReadSavedMostVisited();
NSString* image_title2 = base::SysUTF16ToNSString(image_tile2.title);
NSURL* image_url2 = net::NSURLWithGURL(image_tile2.url);
NTPTile* saved_tile2 = [saved_tiles2 objectForKey:image_url2];
VerifyWithImage(saved_tile2, image_title2, image_url2);
EXPECT_FALSE([[NSFileManager defaultManager]
fileExistsAtPath:[[TestFaviconDirectory()
URLByAppendingPathComponent:saved_tile1
.faviconFileName]
path]]);
}
// Checks the different icon transition for an item.
// Checks that when a fallback exists, it persists even if an image is set.
// Checks that if a new icon is received it replaces the old one.
TEST_F(ContentSuggestionsTileSaverControllerTest, UpdateEntry) {
ntp_tiles::NTPTile tile = ntp_tiles::NTPTile();
// Set up a red favicon.
tile.title = u"Title";
tile.url = GURL("http://url.com");
NSString* ns_title = base::SysUTF16ToNSString(tile.title);
NSURL* ns_url = net::NSURLWithGURL(tile.url);
UIImage* red_image = CreateMockImage([UIColor redColor]);
id mock_favicon_image_fetcher =
OCMClassMock([FaviconAttributesProvider class]);
SetupMockCallback(mock_favicon_image_fetcher, {tile.url}, {});
id mock_favicon_fallback_fetcher =
OCMClassMock([FaviconAttributesProvider class]);
SetupMockCallback(mock_favicon_fallback_fetcher, {}, {tile.url});
ntp_tiles::NTPTilesVector tiles = {
tile,
};
content_suggestions_tile_saver::SaveMostVisitedToDisk(
tiles, mock_favicon_image_fetcher, TestFaviconDirectory());
// Wait for all asynchronous tasks to complete.
scoped_task_evironment_.RunUntilIdle();
NSDictionary<NSURL*, NTPTile*>* saved =
content_suggestions_tile_saver::ReadSavedMostVisited();
NTPTile* saved_tile = [saved objectForKey:ns_url];
VerifyWithImage(saved_tile, ns_title, ns_url);
// Update image to blue
UIImage* blue_image = CreateMockImage([UIColor blueColor]);
EXPECT_NSNE(UIImagePNGRepresentation(red_image),
UIImagePNGRepresentation(blue_image));
content_suggestions_tile_saver::SaveMostVisitedToDisk(
tiles, mock_favicon_image_fetcher, TestFaviconDirectory());
// Wait for all asynchronous tasks to complete.
scoped_task_evironment_.RunUntilIdle();
saved = content_suggestions_tile_saver::ReadSavedMostVisited();
saved_tile = [saved objectForKey:ns_url];
VerifyWithImage(saved_tile, ns_title, ns_url);
// Update with fallback
content_suggestions_tile_saver::SaveMostVisitedToDisk(
tiles, mock_favicon_fallback_fetcher, TestFaviconDirectory());
// Wait for all asynchronous tasks to complete.
scoped_task_evironment_.RunUntilIdle();
saved = content_suggestions_tile_saver::ReadSavedMostVisited();
saved_tile = [saved objectForKey:ns_url];
VerifyWithFallback(saved_tile, ns_title, ns_url);
// Update image to green
UIImage* green_image = CreateMockImage([UIColor greenColor]);
EXPECT_NSNE(UIImagePNGRepresentation(blue_image),
UIImagePNGRepresentation(green_image));
content_suggestions_tile_saver::SaveMostVisitedToDisk(
tiles, mock_favicon_image_fetcher, TestFaviconDirectory());
// Wait for all asynchronous tasks to complete.
scoped_task_evironment_.RunUntilIdle();
saved = content_suggestions_tile_saver::ReadSavedMostVisited();
saved_tile = [saved objectForKey:ns_url];
// Fallback should still be present.
VerifyWithFallbackAndImage(saved_tile, ns_title, ns_url);
// Remove tile.
content_suggestions_tile_saver::SaveMostVisitedToDisk(
ntp_tiles::NTPTilesVector(), mock_favicon_image_fetcher,
TestFaviconDirectory());
// Wait for all asynchronous tasks to complete.
scoped_task_evironment_.RunUntilIdle();
EXPECT_FALSE([[NSFileManager defaultManager]
fileExistsAtPath:[[TestFaviconDirectory()
URLByAppendingPathComponent:saved_tile
.faviconFileName]
path]]);
}
} // anonymous namespace