chromium/ios/chrome/browser/web/model/font_size/font_size_tab_helper_unittest.mm

// Copyright 2018 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/web/model/font_size/font_size_tab_helper.h"

#import <UIKit/UIKit.h>

#import "base/functional/bind.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/scoped_feature_list.h"
#import "components/sync_preferences/pref_service_mock_factory.h"
#import "components/sync_preferences/pref_service_syncable.h"
#import "ios/chrome/browser/shared/model/prefs/browser_prefs.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/web/model/features.h"
#import "ios/chrome/browser/web/model/font_size/font_size_java_script_feature.h"
#import "ios/web/public/test/fakes/fake_web_client.h"
#import "ios/web/public/test/fakes/fake_web_frames_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/test/scoped_testing_web_client.h"
#import "ios/web/public/test/web_state_test_util.h"
#import "ios/web/public/test/web_task_environment.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"

using sync_preferences::PrefServiceMockFactory;
using sync_preferences::PrefServiceSyncable;
using user_prefs::PrefRegistrySyncable;

// Test fixture for FontSizeTabHelper class which creates a FakeWebState.
class FontSizeTabHelperFakeWebStateTest : public PlatformTest {
 public:
  FontSizeTabHelperFakeWebStateTest() {
    scoped_feature_list_.InitAndEnableFeature(
        {web::kWebPageDefaultZoomFromDynamicType});

    // FontSizeTabHelper requires a web frames manager.
    web_state_.SetWebFramesManager(
        FontSizeJavaScriptFeature::GetInstance()->GetSupportedContentWorld(),
        std::make_unique<web::FakeWebFramesManager>());
    FontSizeTabHelper::CreateForWebState(&web_state_);
  }

  FontSizeTabHelperFakeWebStateTest(const FontSizeTabHelperFakeWebStateTest&) =
      delete;
  FontSizeTabHelperFakeWebStateTest& operator=(
      const FontSizeTabHelperFakeWebStateTest&) = delete;

  ~FontSizeTabHelperFakeWebStateTest() override {}

 protected:
  base::test::ScopedFeatureList scoped_feature_list_;
  web::FakeWebState web_state_;
};

// Tests that zoom is only enabled if the page content is html.
TEST_F(FontSizeTabHelperFakeWebStateTest, CanZoomContent) {
  FontSizeTabHelper* font_size_tab_helper =
      FontSizeTabHelper::FromWebState(&web_state_);

  web_state_.SetContentIsHTML(false);
  EXPECT_FALSE(font_size_tab_helper->CurrentPageSupportsTextZoom());

  web_state_.SetContentIsHTML(true);
  EXPECT_TRUE(font_size_tab_helper->CurrentPageSupportsTextZoom());
}

// Test fixture for FontSizeTabHelper class with a real WebState.
class FontSizeTabHelperTest : public PlatformTest {
 public:
  FontSizeTabHelperTest()
      : web_client_(std::make_unique<web::FakeWebClient>()),
        application_(OCMPartialMock([UIApplication sharedApplication])) {
    scoped_feature_list_.InitAndEnableFeature(
        {web::kWebPageDefaultZoomFromDynamicType});

    OCMStub([application_ preferredContentSizeCategory])
        .andDo(^(NSInvocation* invocation) {
          [invocation setReturnValue:&preferred_content_size_category_];
        });
  }

  FontSizeTabHelperTest(const FontSizeTabHelperTest&) = delete;
  FontSizeTabHelperTest& operator=(const FontSizeTabHelperTest&) = delete;

  ~FontSizeTabHelperTest() override { [application_ stopMocking]; }

  void SetUp() override {
    PlatformTest::SetUp();

    browser_state_ = TestChromeBrowserState::Builder().Build();

    web::WebState::CreateParams params(browser_state_.get());
    web_state_ = web::WebState::Create(params);
    web_state_->GetView();
    web_state_->SetKeepRenderProcessAlive(true);

    web::FakeWebClient* web_client =
        static_cast<web::FakeWebClient*>(web_client_.Get());
    web_client->SetJavaScriptFeatures(
        {FontSizeJavaScriptFeature::GetInstance()});

    FontSizeTabHelper::CreateForWebState(web_state());
  }

  void SetPreferredContentSizeCategory(UIContentSizeCategory category) {
    preferred_content_size_category_ = category;
  }

  void SendUIContentSizeCategoryDidChangeNotification() {
    [NSNotificationCenter.defaultCenter
        postNotificationName:UIContentSizeCategoryDidChangeNotification
                      object:nil
                    userInfo:nil];

    base::test::ios::SpinRunLoopWithMinDelay(
        base::test::ios::kSpinDelaySeconds);
  }

  std::string ZoomMultiplierPrefKey(UIContentSizeCategory category, GURL url) {
    return base::StringPrintf(
        "%s.%s", base::SysNSStringToUTF8(category).c_str(), url.host().c_str());
  }

  std::unique_ptr<PrefServiceSyncable> CreatePrefService() {
    scoped_refptr<PrefRegistrySyncable> registry =
        base::MakeRefCounted<PrefRegistrySyncable>();
    // Registers Translate and Language related prefs.
    RegisterBrowserStatePrefs(registry.get());
    PrefServiceMockFactory factory;
    return factory.CreateSyncable(registry.get());
  }

  void LoadWebpage() {
    GURL test_url("https://test.url/");
    LoadWebpage(test_url);
  }

  void LoadWebpage(const GURL& url) {
    web::test::LoadHtml(@"<html><body>Content</body></html>", url, web_state());

    base::test::ios::SpinRunLoopWithMinDelay(
        base::test::ios::kSpinDelaySeconds);
  }

  // Returns the current value of the WebKit text size adjustment style.
  NSString* GetMainFrameTextSizeAdjustment() {
    id result = web::test::ExecuteJavaScript(
        @"document.body.style.webkitTextSizeAdjust", web_state());
    return static_cast<NSString*>(result);
  }

  // Returns the current value of the WebKit text size adjustment style.
  NSString* GetIframeTextSizeAdjusment() {
    id result = web::test::ExecuteJavaScript(
        @"window.frames[0].document.body.style.webkitTextSizeAdjust",
        web_state());
    return static_cast<NSString*>(result);
  }

  int GetCurrentFontSizeFromString(NSString* adjustment) {
    if (!adjustment || !adjustment.length) {
      return 0;
    }
    NSRange percentCharRange = NSMakeRange(adjustment.length - 1, 1);
    NSString* fontSizePercentage =
        [adjustment stringByReplacingCharactersInRange:percentCharRange
                                            withString:@""];
    return [fontSizePercentage intValue];
  }

  // Waits for the text size adjustment value of the main frame to be
  // `adjustment`. Returns true if the value matches `adjustment` within
  // `kWaitForJSCompletionTimeout`, false otherwise.
  bool WaitForMainFrameTextSizeAdjustmentEqualTo(int adjustment) {
    return base::test::ios::WaitUntilConditionOrTimeout(
        base::test::ios::kWaitForJSCompletionTimeout, ^bool {
          return GetCurrentFontSizeFromString(
                     GetMainFrameTextSizeAdjustment()) == adjustment;
        });
  }

 protected:
  web::WebState* web_state() { return web_state_.get(); }

  web::ScopedTestingWebClient web_client_;
  web::WebTaskEnvironment task_environment_;
  std::unique_ptr<TestChromeBrowserState> browser_state_;
  std::unique_ptr<web::WebState> web_state_;

  base::test::ScopedFeatureList scoped_feature_list_;
  UIContentSizeCategory preferred_content_size_category_ =
      UIContentSizeCategoryLarge;
  id application_ = nil;
};

// Tests that a web page's font size is set properly in a procedure started
// with default `UIApplication.sharedApplication.preferredContentSizeCategory`.
TEST_F(FontSizeTabHelperTest, PageLoadedWithDefaultFontSize) {
  LoadWebpage();
  ASSERT_EQ(0ul, [GetMainFrameTextSizeAdjustment() length]);

  // Change PreferredContentSizeCategory and send
  // UIContentSizeCategoryDidChangeNotification.
  preferred_content_size_category_ = UIContentSizeCategoryExtraLarge;
  SendUIContentSizeCategoryDidChangeNotification();

  EXPECT_TRUE(WaitForMainFrameTextSizeAdjustmentEqualTo(112));

  // Reload web page, font size should be set to the same previous value.
  LoadWebpage();

  EXPECT_TRUE(WaitForMainFrameTextSizeAdjustmentEqualTo(112));
}

// Tests that a web page's font size is set properly in a procedure started
// with special `UIApplication.sharedApplication.preferredContentSizeCategory`.
TEST_F(FontSizeTabHelperTest, PageLoadedWithExtraLargeFontSize) {
  preferred_content_size_category_ = UIContentSizeCategoryExtraLarge;

  LoadWebpage();
  EXPECT_TRUE(WaitForMainFrameTextSizeAdjustmentEqualTo(112));

  // Change PreferredContentSizeCategory and send
  // UIContentSizeCategoryDidChangeNotification.
  preferred_content_size_category_ = UIContentSizeCategoryExtraExtraLarge;
  SendUIContentSizeCategoryDidChangeNotification();
  EXPECT_TRUE(WaitForMainFrameTextSizeAdjustmentEqualTo(124));

  // Reload web page, font size should be set to the same previous value.
  LoadWebpage();
  EXPECT_TRUE(WaitForMainFrameTextSizeAdjustmentEqualTo(124));
}

// Tests that UMA log is sent when
// `UIApplication.sharedApplication.preferredContentSizeCategory` returns an
// unrecognizable category.
TEST_F(FontSizeTabHelperTest, PageLoadedWithUnrecognizableFontSize) {
  preferred_content_size_category_ = @"This is a new Category";

  LoadWebpage();
  ASSERT_EQ(0ul, [GetMainFrameTextSizeAdjustment() length]);

  // Change PreferredContentSizeCategory and send
  // UIContentSizeCategoryDidChangeNotification.
  preferred_content_size_category_ = UIContentSizeCategoryExtraExtraLarge;
  SendUIContentSizeCategoryDidChangeNotification();

  EXPECT_TRUE(WaitForMainFrameTextSizeAdjustmentEqualTo(124));

  // Reload web page, font size should be set to the same previous value.
  LoadWebpage();

  EXPECT_TRUE(WaitForMainFrameTextSizeAdjustmentEqualTo(124));
}

// Tests that the font size is changed in all frames on the page.
TEST_F(FontSizeTabHelperTest, ZoomInAllFrames) {
  preferred_content_size_category_ = UIContentSizeCategoryExtraLarge;

  web::test::LoadHtml(@"<html><body>Content<iframe srcdoc=\"<html>iFrame "
                      @"Content</html>\"></iframe></body></html>",
                      web_state());

  EXPECT_TRUE(WaitForMainFrameTextSizeAdjustmentEqualTo(112));

  EXPECT_EQ(112, GetCurrentFontSizeFromString(GetIframeTextSizeAdjusment()));
}

// Tests that the user can zoom in, and that after zooming in, the correct
// Javascript has been executed, and the zoom value is stored in the user pref
// under the correct key.
TEST_F(FontSizeTabHelperTest, ZoomIn) {
  GURL test_url("https://test.url/");
  LoadWebpage(test_url);

  FontSizeTabHelper* font_size_tab_helper =
      FontSizeTabHelper::FromWebState(web_state());
  font_size_tab_helper->UserZoom(ZOOM_IN);

  EXPECT_TRUE(font_size_tab_helper->CanUserZoomIn());

  EXPECT_TRUE(WaitForMainFrameTextSizeAdjustmentEqualTo(110));

  // Check that new zoom value is also saved in the pref under the right key.
  std::string pref_key =
      ZoomMultiplierPrefKey(preferred_content_size_category_, test_url);
  const base::Value::Dict& pref =
      browser_state_->GetPrefs()->GetDict(prefs::kIosUserZoomMultipliers);
  EXPECT_EQ(1.1, pref.FindDoubleByDottedPath(pref_key));
}

// Tests that the user can zoom out, and that after zooming out, the correct
// Javascript has been executed, and the zoom value is stored in the user pref
// under the correct key.
TEST_F(FontSizeTabHelperTest, ZoomOut) {
  GURL test_url("https://test.url/");
  LoadWebpage(test_url);

  FontSizeTabHelper* font_size_tab_helper =
      FontSizeTabHelper::FromWebState(web_state());
  font_size_tab_helper->UserZoom(ZOOM_OUT);

  EXPECT_TRUE(font_size_tab_helper->CanUserZoomOut());

  EXPECT_TRUE(WaitForMainFrameTextSizeAdjustmentEqualTo(90));

  // Check that new zoom value is also saved in the pref under the right key.
  std::string pref_key =
      ZoomMultiplierPrefKey(preferred_content_size_category_, test_url);
  const base::Value::Dict& pref =
      browser_state_->GetPrefs()->GetDict(prefs::kIosUserZoomMultipliers);
  EXPECT_EQ(0.9, pref.FindDoubleByDottedPath(pref_key));
}

// Tests after resetting the zoom level, the correct Javascript has been
// executed, and the value in the user prefs for the key has ben cleared.
TEST_F(FontSizeTabHelperTest, ResetZoom) {
  GURL test_url("https://test.url/");
  LoadWebpage(test_url);

  // First zoom in to setup the reset.
  FontSizeTabHelper* font_size_tab_helper =
      FontSizeTabHelper::FromWebState(web_state());
  font_size_tab_helper->UserZoom(ZOOM_IN);
  std::string pref_key =
      ZoomMultiplierPrefKey(preferred_content_size_category_, test_url);
  const base::Value::Dict& pref =
      browser_state_->GetPrefs()->GetDict(prefs::kIosUserZoomMultipliers);
  EXPECT_EQ(1.1, pref.FindDoubleByDottedPath(pref_key));

  // Then reset. The pref key should be removed from the dictionary.
  font_size_tab_helper->UserZoom(ZOOM_RESET);

  EXPECT_TRUE(WaitForMainFrameTextSizeAdjustmentEqualTo(100));
  EXPECT_FALSE(pref.FindDoubleByDottedPath(pref_key));
}

// Tests that when the user changes the accessibility content size category and
// zooms, the resulting zoom level is the multiplication of both parts.
TEST_F(FontSizeTabHelperTest, ZoomAndAccessibilityTextSize) {
  preferred_content_size_category_ = UIContentSizeCategoryExtraLarge;

  GURL test_url("https://test.url/");
  LoadWebpage(test_url);

  FontSizeTabHelper* font_size_tab_helper =
      FontSizeTabHelper::FromWebState(web_state());
  font_size_tab_helper->UserZoom(ZOOM_IN);

  std::string pref_key =
      ZoomMultiplierPrefKey(preferred_content_size_category_, test_url);
  // 1.12 from accessibility * 1.1 from zoom
  EXPECT_TRUE(WaitForMainFrameTextSizeAdjustmentEqualTo(123));
  // Only the user zoom portion is stored in the preferences.
  const base::Value::Dict& pref =
      browser_state_->GetPrefs()->GetDict(prefs::kIosUserZoomMultipliers);
  EXPECT_EQ(1.1, pref.FindDoubleByDottedPath(pref_key));
}

// Tests that the user pref is cleared when requested.
TEST_F(FontSizeTabHelperTest, ClearUserZoomPrefs) {
  GURL test_url("https://test.url/");
  LoadWebpage(test_url);

  FontSizeTabHelper* font_size_tab_helper =
      FontSizeTabHelper::FromWebState(web_state());
  font_size_tab_helper->UserZoom(ZOOM_IN);
  base::test::ios::SpinRunLoopWithMinDelay(base::test::ios::kSpinDelaySeconds);

  // Make sure the first value is stored in the pref store.
  const base::Value::Dict& pref =
      browser_state_->GetPrefs()->GetDict(prefs::kIosUserZoomMultipliers);
  std::string pref_key =
      ZoomMultiplierPrefKey(preferred_content_size_category_, test_url);
  EXPECT_EQ(1.1, pref.FindDoubleByDottedPath(pref_key));

  FontSizeTabHelper::ClearUserZoomPrefs(browser_state_->GetPrefs());

  EXPECT_TRUE(browser_state_.get()
                  ->GetPrefs()
                  ->GetDict(prefs::kIosUserZoomMultipliers)
                  .empty());
}

TEST_F(FontSizeTabHelperTest, GoogleCachedAMPPageHasSeparateKey) {
  // First, zoom in on a regular Google url.
  GURL google_url("https://www.google.com/");
  LoadWebpage(google_url);
  FontSizeTabHelper* font_size_tab_helper =
      FontSizeTabHelper::FromWebState(web_state());
  font_size_tab_helper->UserZoom(ZOOM_IN);
  std::string google_pref_key =
      ZoomMultiplierPrefKey(preferred_content_size_category_, google_url);

  // Next, zoom out on a Google AMP url and make sure both states are saved
  // separately.
  GURL google_amp_url("https://www.google.com/amp/s/www.france24.com");
  LoadWebpage(google_amp_url);
  font_size_tab_helper->UserZoom(ZOOM_OUT);
  // Google AMP pages use a different key for the URL part.
  std::string google_amp_pref_key = base::StringPrintf(
      "%s.%s",
      base::SysNSStringToUTF8(preferred_content_size_category_).c_str(),
      "www.google.com/amp");

  EXPECT_NE(google_pref_key, google_amp_pref_key);

  const base::Value::Dict& pref =
      browser_state_->GetPrefs()->GetDict(prefs::kIosUserZoomMultipliers);
  EXPECT_EQ(1.1, pref.FindDoubleByDottedPath(google_pref_key));
  EXPECT_EQ(0.9, pref.FindDoubleByDottedPath(google_amp_pref_key));
}