chromium/chrome/browser/offline_pages/recent_tab_helper_unittest.cc

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/offline_pages/recent_tab_helper.h"

#include <memory>
#include <string>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_mock_time_message_loop_task_runner.h"
#include "chrome/browser/offline_pages/offline_page_model_factory.h"
#include "chrome/browser/offline_pages/request_coordinator_factory.h"
#include "chrome/browser/offline_pages/test_offline_page_model_builder.h"
#include "chrome/browser/offline_pages/test_request_coordinator_builder.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_profile.h"
#include "components/offline_pages/core/client_namespace_constants.h"
#include "components/offline_pages/core/offline_page_feature.h"
#include "components/offline_pages/core/offline_page_item.h"
#include "components/offline_pages/core/offline_page_model.h"
#include "components/offline_pages/core/offline_page_test_archiver.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/navigation_simulator.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"

namespace offline_pages {

namespace {

const int kTabId = 153;

}  // namespace

class TestDelegate: public RecentTabHelper::Delegate {
 public:
  const size_t kArchiveSizeToReport = 1234;

  explicit TestDelegate(
      OfflinePageTestArchiver::Observer* observer,
      int tab_id,
      bool tab_id_result);
  ~TestDelegate() override {}

  std::unique_ptr<OfflinePageArchiver> CreatePageArchiver(
        content::WebContents* web_contents) override;
    // There is no expectations that tab_id is always present.
  bool GetTabId(content::WebContents* web_contents, int* tab_id) override;
  bool IsLowEndDevice() override { return is_low_end_device_; }
  bool IsCustomTab(content::WebContents* web_contents) override {
    return is_custom_tab_;
  }

  void set_archive_result(
      offline_pages::OfflinePageArchiver::ArchiverResult result) {
    archive_result_ = result;
  }

  void set_archive_size(int64_t size) { archive_size_ = size; }

  void SetAsLowEndDevice() { is_low_end_device_ = true; }

  void set_is_custom_tab(bool is_custom_tab) { is_custom_tab_ = is_custom_tab; }

 private:
  raw_ptr<OfflinePageTestArchiver::Observer> observer_;  // observer owns this.
  int tab_id_;
  bool tab_id_result_;

  // These values can be updated so that new OfflinePageTestArchiver instances
  // will return different results.
  offline_pages::OfflinePageArchiver::ArchiverResult archive_result_ =
      offline_pages::OfflinePageArchiver::ArchiverResult::SUCCESSFULLY_CREATED;
  int64_t archive_size_ = kArchiveSizeToReport;
  bool is_low_end_device_ = false;
  bool is_custom_tab_ = false;
};

class RecentTabHelperTest
    : public ChromeRenderViewHostTestHarness,
      public OfflinePageModel::Observer,
      public OfflinePageTestArchiver::Observer {
 public:
  RecentTabHelperTest();

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

  ~RecentTabHelperTest() override {}

  void SetUp() override;
  void TearDown() override;
  const std::vector<OfflinePageItem>& GetAllPages();

  void FailLoad(const GURL& url);

  // Runs main thread.
  void RunUntilIdle();
  // Advances main thread time to trigger the snapshot controller's timeouts.
  void FastForwardSnapshotController();

  void NavigateAndCommit(const GURL& url);
  void Reload();

  // Navigates to the URL and commit as if it has been typed in the address bar.
  // Note: we need this to simulate navigations to the same URL that more like a
  // reload and not same page. NavigateAndCommit simulates a click on a link
  // and when reusing the same URL that will be considered a same page
  // navigation.
  void NavigateAndCommitTyped(const GURL& url);

  // Navigates to the URL and commit as if a form had been submitted.
  void NavigateAndCommitPost(const GURL& url);

  ClientId NewDownloadClientId();

  RecentTabHelper* recent_tab_helper() const { return recent_tab_helper_; }

  OfflinePageModel* model() const { return model_; }

  TestDelegate* default_test_delegate() { return default_test_delegate_; }

  base::HistogramTester* histogram_tester() { return histogram_tester_.get(); }

  // Returns a OfflinePageItem pointer from |all_pages| that matches the
  // provided |offline_id|. If a match is not found returns nullptr.
  const OfflinePageItem* FindPageForOfflineId(int64_t offline_id) {
    for (const OfflinePageItem& page : GetAllPages()) {
      if (page.offline_id == offline_id)
        return &page;
    }
    return nullptr;
  }

  size_t page_added_count() { return page_added_count_; }
  size_t model_removed_count() { return model_removed_count_; }

  // OfflinePageModel::Observer
  void OfflinePageModelLoaded(OfflinePageModel* model) override {
    all_pages_needs_updating_ = true;
  }
  void OfflinePageAdded(OfflinePageModel* model,
                        const OfflinePageItem& added_page) override {
    page_added_count_++;
    all_pages_needs_updating_ = true;
  }
  void OfflinePageDeleted(const OfflinePageItem& item) override {
    model_removed_count_++;
    all_pages_needs_updating_ = true;
  }

  // OfflinePageTestArchiver::Observer
  void SetLastPathCreatedByArchiver(const base::FilePath& file_path) override {}

 private:
  void StartAndCommitNavigation(
      std::unique_ptr<content::NavigationSimulator> simulator);

  void OnGetAllPagesDone(const std::vector<OfflinePageItem>& result);

  raw_ptr<RecentTabHelper> recent_tab_helper_;   // Owned by WebContents.
  raw_ptr<OfflinePageModel> model_;              // Keyed service.
  raw_ptr<TestDelegate> default_test_delegate_;  // Created at SetUp.
  size_t page_added_count_;
  size_t model_removed_count_;
  std::vector<OfflinePageItem> all_pages_;
  bool all_pages_needs_updating_;
  std::unique_ptr<base::HistogramTester> histogram_tester_;

  // Mocks the RenderViewHostTestHarness' main thread runner. Needs to be delay
  // initialized in SetUp() -- can't be a simple member -- since
  // RenderViewHostTestHarness only initializes its main thread environment in
  // its SetUp() :(.
  std::unique_ptr<base::ScopedMockTimeMessageLoopTaskRunner>
      mocked_main_runner_;

  base::WeakPtrFactory<RecentTabHelperTest> weak_ptr_factory_{this};
};

TestDelegate::TestDelegate(
    OfflinePageTestArchiver::Observer* observer,
    int tab_id,
    bool tab_id_result)
    : observer_(observer),
      tab_id_(tab_id),
      tab_id_result_(tab_id_result) {
}

std::unique_ptr<OfflinePageArchiver> TestDelegate::CreatePageArchiver(
    content::WebContents* web_contents) {
  std::unique_ptr<OfflinePageTestArchiver> archiver(new OfflinePageTestArchiver(
      observer_, web_contents->GetLastCommittedURL(), archive_result_,
      std::u16string(), kArchiveSizeToReport, std::string(),
      base::SingleThreadTaskRunner::GetCurrentDefault()));
  return std::move(archiver);
}

// There is no expectations that tab_id is always present.
bool TestDelegate::GetTabId(content::WebContents* web_contents, int* tab_id) {
  *tab_id = tab_id_;
  return tab_id_result_;
}

RecentTabHelperTest::RecentTabHelperTest()
    : recent_tab_helper_(nullptr),
      model_(nullptr),
      default_test_delegate_(nullptr),
      page_added_count_(0),
      model_removed_count_(0),
      all_pages_needs_updating_(true) {}

void RecentTabHelperTest::SetUp() {
  ChromeRenderViewHostTestHarness::SetUp();

  mocked_main_runner_ =
      std::make_unique<base::ScopedMockTimeMessageLoopTaskRunner>();

  // Sets up the factories for testing.
  OfflinePageModelFactory::GetInstance()->SetTestingFactoryAndUse(
      profile()->GetProfileKey(),
      base::BindRepeating(&BuildTestOfflinePageModel));
  RunUntilIdle();
  RequestCoordinatorFactory::GetInstance()->SetTestingFactoryAndUse(
      profile(), base::BindRepeating(&BuildTestRequestCoordinator));
  RunUntilIdle();

  RecentTabHelper::CreateForWebContents(web_contents());
  recent_tab_helper_ = RecentTabHelper::FromWebContents(web_contents());

  std::unique_ptr<TestDelegate> test_delegate(
      new TestDelegate(this, kTabId, true));
  default_test_delegate_ = test_delegate.get();
  recent_tab_helper_->SetDelegate(std::move(test_delegate));

  model_ = OfflinePageModelFactory::GetForBrowserContext(browser_context());
  model_->AddObserver(this);

  histogram_tester_ = std::make_unique<base::HistogramTester>();
}

void RecentTabHelperTest::TearDown() {
  mocked_main_runner_.reset();
  ChromeRenderViewHostTestHarness::TearDown();
}

void RecentTabHelperTest::FailLoad(const GURL& url) {
  content::NavigationSimulator::NavigateAndFailFromBrowser(
      web_contents(), url, net::ERR_INTERNET_DISCONNECTED);
}

const std::vector<OfflinePageItem>& RecentTabHelperTest::GetAllPages() {
  if (all_pages_needs_updating_) {
    model()->GetAllPages(base::BindOnce(&RecentTabHelperTest::OnGetAllPagesDone,
                                        weak_ptr_factory_.GetWeakPtr()));
    RunUntilIdle();
    all_pages_needs_updating_ = false;
  }
  return all_pages_;
}

void RecentTabHelperTest::OnGetAllPagesDone(
    const std::vector<OfflinePageItem>& result) {
  all_pages_ = result;
}

void RecentTabHelperTest::RunUntilIdle() {
  (*mocked_main_runner_)->RunUntilIdle();
}

void RecentTabHelperTest::FastForwardSnapshotController() {
  constexpr base::TimeDelta kLongDelay = base::Seconds(100);
  (*mocked_main_runner_)->FastForwardBy(kLongDelay);
}

void RecentTabHelperTest::StartAndCommitNavigation(
    std::unique_ptr<content::NavigationSimulator> simulator) {
  simulator->SetAutoAdvance(false);
  simulator->SetKeepLoading(true);
  simulator->Start();

  // Need to flush the task queue manually since there may be async tasks
  // spawned by navigation start that must finish before commit. Since this test
  // harness swaps out the main thread, NavigationSimulator cannot pump the task
  // queue itself to finish navigations.
  //
  // TODO(csharrison): This can probably be removed and replaced with either the
  // NavigationSimulator controlling the mock task runner, or by the snapshot
  // controller using a (mock) timer instead of PostDelayedTask.
  RunUntilIdle();
  simulator->Commit();
}

void RecentTabHelperTest::NavigateAndCommit(const GURL& url) {
  StartAndCommitNavigation(content::NavigationSimulator::CreateBrowserInitiated(
      url, web_contents()));
}

void RecentTabHelperTest::Reload() {
  auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
      web_contents()->GetLastCommittedURL(), web_contents());
  simulator->SetReloadType(content::ReloadType::NORMAL);
  StartAndCommitNavigation(std::move(simulator));
}

void RecentTabHelperTest::NavigateAndCommitTyped(const GURL& url) {
  auto simulator =
      content::NavigationSimulator::CreateBrowserInitiated(url, web_contents());
  simulator->SetTransition(ui::PAGE_TRANSITION_TYPED);
  StartAndCommitNavigation(std::move(simulator));
}

void RecentTabHelperTest::NavigateAndCommitPost(const GURL& url) {
  auto simulator =
      content::NavigationSimulator::CreateRendererInitiated(url, main_rfh());
  simulator->SetMethod("POST");
  simulator->SetTransition(ui::PAGE_TRANSITION_FORM_SUBMIT);
  StartAndCommitNavigation(std::move(simulator));
}

ClientId RecentTabHelperTest::NewDownloadClientId() {
  static int counter = 0;
  return ClientId(kDownloadNamespace,
                  std::string("id") + base::NumberToString(++counter));
}

// Checks the test setup.
TEST_F(RecentTabHelperTest, RecentTabHelperInstanceExists) {
  EXPECT_NE(nullptr, recent_tab_helper());
}

// Fully loads a page then simulates the tab being hidden. Verifies that a
// snapshot is created only when the latter happens.
TEST_F(RecentTabHelperTest, LastNCaptureAfterLoad) {
  // Navigate and finish loading. Nothing should be saved.
  const GURL kTestUrl("http://mystery.site/foo.html");
  NavigateAndCommit(kTestUrl);

  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  // Move the snapshot controller's time forward so it gets past timeouts.
  FastForwardSnapshotController();
  EXPECT_EQ(0U, page_added_count());
  ASSERT_EQ(0U, GetAllPages().size());

  // Tab is hidden with a fully loaded page. A snapshot save should happen.
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(1U, page_added_count());
  ASSERT_EQ(1U, GetAllPages().size());
  EXPECT_EQ(kTestUrl, GetAllPages()[0].url);
  EXPECT_EQ(kLastNNamespace, GetAllPages()[0].client_id.name_space);
}

// Simulates the tab being hidden too early in the page loading so that a
// snapshot should not be created.
TEST_F(RecentTabHelperTest, NoLastNCaptureIfTabHiddenTooEarlyInPageLoad) {
  // Commit the navigation and hide the tab. Nothing should be saved.
  NavigateAndCommit(GURL("http://mystery.site/foo.html"));
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(0U, page_added_count());
  ASSERT_EQ(0U, GetAllPages().size());

  // Then allow the page to fully load. Nothing should be saved.
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  // Move the snapshot controller's time forward so it gets past timeouts.
  FastForwardSnapshotController();
  EXPECT_EQ(0U, page_added_count());
  ASSERT_EQ(0U, GetAllPages().size());
}

// Checks that WebContents with no tab IDs have snapshot requests properly
// ignored from both last_n and downloads.
TEST_F(RecentTabHelperTest, NoTabIdNoCapture) {
  // Create delegate that returns 'false' as TabId retrieval result.
  recent_tab_helper()->SetDelegate(
      std::make_unique<TestDelegate>(this, kTabId, false));

  NavigateAndCommit(GURL("http://mystery.site/foo.html"));
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  recent_tab_helper()->ObserveAndDownloadCurrentPage(NewDownloadClientId(),
                                                     123L, "");
  RunUntilIdle();
  // No page should be captured.
  EXPECT_EQ(0U, page_added_count());
  ASSERT_EQ(0U, GetAllPages().size());
}

// Checks that last_n is disabled if the device is low-end (aka svelte) but that
// download requests still work.
TEST_F(RecentTabHelperTest, LastNDisabledOnSvelte) {
  // Simulates a low end device.
  default_test_delegate()->SetAsLowEndDevice();

  // Navigate and finish loading then hide the tab. Nothing should be saved.
  NavigateAndCommit(GURL("http://mystery.site/foo.html"));

  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(0U, page_added_count());
  ASSERT_EQ(0U, GetAllPages().size());

  // But the following download request should work normally
  recent_tab_helper()->ObserveAndDownloadCurrentPage(NewDownloadClientId(),
                                                     123L, "");
  RunUntilIdle();
  EXPECT_EQ(1U, page_added_count());
  ASSERT_EQ(1U, GetAllPages().size());
}

// Checks that last_n will not save a snapshot while the tab is being presented
// as a custom tab. Download requests should be unaffected though.
TEST_F(RecentTabHelperTest, LastNWontSaveCustomTab) {
  // Simulates the tab running as a custom tab.
  default_test_delegate()->set_is_custom_tab(true);

  // Navigate and finish loading then hide the tab. Nothing should be saved.
  NavigateAndCommit(GURL("http://mystery.site/foo.html"));
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(0U, page_added_count());
  ASSERT_EQ(0U, GetAllPages().size());

  // But the following download request should work normally
  recent_tab_helper()->ObserveAndDownloadCurrentPage(NewDownloadClientId(),
                                                     123L, "");
  RunUntilIdle();
  EXPECT_EQ(1U, page_added_count());
  ASSERT_EQ(1U, GetAllPages().size());

  // Simulates the tab being transfered from the CustomTabActivity back to a
  // ChromeActivity.
  default_test_delegate()->set_is_custom_tab(false);

  // Upon the next hide a last_n snapshot should be saved.
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(2U, page_added_count());
  ASSERT_EQ(2U, GetAllPages().size());
}

// Triggers two last_n snapshot captures during a single page load. Should end
// up with one snapshot, the 1st being replaced by the 2nd.
TEST_F(RecentTabHelperTest, TwoCapturesSamePageLoad) {
  const GURL kTestUrl("http://mystery.site/foo.html");
  NavigateAndCommit(kTestUrl);

  // Set page loading state to the 1st snapshot-able stage. No capture so far.
  recent_tab_helper()->PrimaryMainDocumentElementAvailable();
  FastForwardSnapshotController();
  EXPECT_EQ(0U, page_added_count());

  // Tab is hidden and a snapshot should be saved.
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(1U, page_added_count());
  EXPECT_EQ(0U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
  EXPECT_EQ(kTestUrl, GetAllPages()[0].url);
  int64_t first_offline_id = GetAllPages()[0].offline_id;

  // Set page loading state to the 2nd and last snapshot-able stage. No new
  // capture should happen.
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  EXPECT_EQ(1U, page_added_count());
  EXPECT_EQ(0U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());

  // Tab is hidden again. At this point a higher quality snapshot is expected so
  // a new one should be captured and replace the previous one.
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(2U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
  EXPECT_EQ(kTestUrl, GetAllPages()[0].url);
  EXPECT_NE(first_offline_id, GetAllPages()[0].offline_id);
}

// Triggers two last_n captures during a single page load, where the 2nd capture
// fails. Should end up with one offline page (the 1st, successful snapshot
// should be kept).
// TODO(carlosk): re-enable once https://crbug.com/705079 is fixed.
TEST_F(RecentTabHelperTest, DISABLED_TwoCapturesWhere2ndFailsSamePageLoad) {
  // Navigate and load until the 1st stage. Tab hidden should trigger a capture.
  const GURL kTestUrl("http://mystery.site/foo.html");
  NavigateAndCommit(kTestUrl);
  recent_tab_helper()->PrimaryMainDocumentElementAvailable();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(1U, page_added_count());
  EXPECT_EQ(0U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
  EXPECT_EQ(kTestUrl, GetAllPages()[0].url);
  int64_t first_offline_id = GetAllPages()[0].offline_id;

  // Updates the delegate so that will make the second snapshot fail.
  default_test_delegate()->set_archive_size(-1);
  default_test_delegate()->set_archive_result(
      offline_pages::OfflinePageArchiver::ArchiverResult::
          ERROR_ARCHIVE_CREATION_FAILED);

  // Advance loading to the 2nd and final stage and then hide the tab. A new
  // capture is requested but its creation will fail. The exact same snapshot
  // from before should still be available.
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(1U, page_added_count());
  EXPECT_EQ(0U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
  EXPECT_EQ(kTestUrl, GetAllPages()[0].url);
  EXPECT_EQ(first_offline_id, GetAllPages()[0].offline_id);
}

// Triggers two last_n captures for two different loads of the same URL (aka
// reload). Should end up with a single snapshot (from the 2nd load).
TEST_F(RecentTabHelperTest, TwoCapturesDifferentPageLoadsSameUrl) {
  // Fully load the page. Hide the tab and check for a snapshot.
  const GURL kTestUrl("http://mystery.site/foo.html");
  NavigateAndCommit(kTestUrl);
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(1U, page_added_count());
  EXPECT_EQ(0U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
  EXPECT_EQ(kTestUrl, GetAllPages()[0].url);
  int64_t first_offline_id = GetAllPages()[0].offline_id;

  // Reload the same URL until the page is minimally loaded. The previous
  // snapshot should have been removed.
  NavigateAndCommitTyped(kTestUrl);
  recent_tab_helper()->PrimaryMainDocumentElementAvailable();
  FastForwardSnapshotController();
  EXPECT_EQ(1U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(0U, GetAllPages().size());

  // Hide the tab and a new snapshot should be taken.
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(2U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
  EXPECT_EQ(kTestUrl, GetAllPages()[0].url);
  EXPECT_NE(first_offline_id, GetAllPages()[0].offline_id);
}

// Triggers two last_n captures for two different page loads of the same URL
// (aka reload), where the 2nd capture fails. Should end up with no offline
// pages (a privacy driven decision).
TEST_F(RecentTabHelperTest, TwoCapturesWhere2ndFailsDifferentPageLoadsSameUrl) {
  // Fully load the page then hide the tab. A capture is expected.
  const GURL kTestUrl("http://mystery.site/foo.html");
  NavigateAndCommit(kTestUrl);
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(1U, page_added_count());
  EXPECT_EQ(0U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
  EXPECT_EQ(kTestUrl, GetAllPages()[0].url);

  // Updates the delegate so that will make the second snapshot fail.
  default_test_delegate()->set_archive_size(-1);
  default_test_delegate()->set_archive_result(
      offline_pages::OfflinePageArchiver::ArchiverResult::
          ERROR_ARCHIVE_CREATION_FAILED);

  // Fully load the page once more then hide the tab again. A capture happens
  // and fails but no snapshot should remain.
  NavigateAndCommitTyped(kTestUrl);
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(1U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(0U, GetAllPages().size());
}

// Triggers two last_n captures for two different page loads of different URLs.
// Should end up with a single snapshot of the last page.
TEST_F(RecentTabHelperTest, TwoCapturesDifferentPageLoadsDifferentUrls) {
  // Fully load the first URL then hide the tab and check for a snapshot.
  const GURL kTestUrl("http://mystery.site/foo.html");
  NavigateAndCommit(kTestUrl);

  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(1U, page_added_count());
  EXPECT_EQ(0U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
  EXPECT_EQ(kTestUrl, GetAllPages()[0].url);

  // Fully load the second URL. The previous snapshot should have been deleted.0
  const GURL kOtherUrl("http://crazy.site/foo_other.html");
  NavigateAndCommitTyped(kOtherUrl);

  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  EXPECT_EQ(1U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(0U, GetAllPages().size());

  // Then hide the tab and check for a single snapshot of the new page.
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(2U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
  EXPECT_EQ(kOtherUrl, GetAllPages()[0].url);
}

// Fully loads a page where last_n captures two snapshots. Then triggers two
// snapshot requests by downloads. Should end up with three offline pages: one
// from last_n (2nd replaces the 1st) and two from downloads (which shouldn't
// replace each other).
TEST_F(RecentTabHelperTest, TwoLastNAndTwoDownloadCapturesSamePage) {
  // Fully loads the page with intermediary steps where the tab is hidden. Then
  // check that two last_n snapshots were created but only one was kept.
  const GURL kTestUrl("http://mystery.site/foo.html");
  NavigateAndCommit(kTestUrl);
  recent_tab_helper()->PrimaryMainDocumentElementAvailable();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(2U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
  EXPECT_EQ(kTestUrl, GetAllPages()[0].url);
  int64_t first_offline_id = GetAllPages()[0].offline_id;

  // First snapshot request by downloads. Two offline pages are expected.
  const int64_t second_offline_id = first_offline_id + 1;
  const ClientId second_client_id = NewDownloadClientId();
  recent_tab_helper()->ObserveAndDownloadCurrentPage(second_client_id,
                                                     second_offline_id, "");
  RunUntilIdle();
  EXPECT_EQ(3U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(2U, GetAllPages().size());
  EXPECT_NE(nullptr, FindPageForOfflineId(first_offline_id));
  const OfflinePageItem* second_page = FindPageForOfflineId(second_offline_id);
  ASSERT_NE(nullptr, second_page);
  EXPECT_EQ(kTestUrl, second_page->url);
  EXPECT_EQ(second_client_id, second_page->client_id);

  // Second snapshot request by downloads. Three offline pages are expected.
  const int64_t third_offline_id = first_offline_id + 2;
  const ClientId third_client_id = NewDownloadClientId();
  recent_tab_helper()->ObserveAndDownloadCurrentPage(third_client_id,
                                                     third_offline_id, "");
  RunUntilIdle();
  EXPECT_EQ(4U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(3U, GetAllPages().size());
  EXPECT_NE(nullptr, FindPageForOfflineId(first_offline_id));
  EXPECT_NE(nullptr, FindPageForOfflineId(second_offline_id));
  const OfflinePageItem* third_page = FindPageForOfflineId(third_offline_id);
  ASSERT_NE(nullptr, third_page);
  EXPECT_EQ(kTestUrl, third_page->url);
  EXPECT_EQ(third_client_id, third_page->client_id);
}

// Simulates an error (disconnection) during the load of a page. Should end up
// with no offline pages for any requester.
TEST_F(RecentTabHelperTest, NoCaptureOnErrorPage) {
  FailLoad(GURL("http://mystery.site/foo.html"));
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  recent_tab_helper()->ObserveAndDownloadCurrentPage(NewDownloadClientId(),
                                                     123L, "");
  RunUntilIdle();
  ASSERT_EQ(0U, GetAllPages().size());
}

// Simulates a download request to offline the current page made early during
// loading. Should execute two captures but only the final one is kept.
TEST_F(RecentTabHelperTest, DownloadRequestEarlyInLoad) {
  // Commit the navigation and request the snapshot from downloads. No captures
  // so far.
  const GURL kTestUrl("http://mystery.site/foo.html");
  NavigateAndCommit(kTestUrl);
  const ClientId client_id = NewDownloadClientId();
  recent_tab_helper()->ObserveAndDownloadCurrentPage(client_id, 153L, "");
  FastForwardSnapshotController();
  ASSERT_EQ(0U, GetAllPages().size());

  // Minimally load the page. First capture should occur.
  recent_tab_helper()->PrimaryMainDocumentElementAvailable();
  FastForwardSnapshotController();
  ASSERT_EQ(1U, GetAllPages().size());
  const OfflinePageItem& early_page = GetAllPages()[0];
  EXPECT_EQ(kTestUrl, early_page.url);
  EXPECT_EQ(client_id, early_page.client_id);
  EXPECT_EQ(153L, early_page.offline_id);

  // Fully load the page. A second capture should replace the first one.
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  EXPECT_EQ(2U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
  const OfflinePageItem& later_page = GetAllPages()[0];
  EXPECT_EQ(kTestUrl, later_page.url);
  EXPECT_EQ(client_id, later_page.client_id);
  EXPECT_EQ(153L, later_page.offline_id);
}

// Simulates a download request to offline the current page made when the page
// is minimally loaded. Should execute two captures but only the final one is
// kept.
TEST_F(RecentTabHelperTest, DownloadRequestLaterInLoad) {
  const GURL kTestUrl("http://mystery.site/foo.html");
  NavigateAndCommit(kTestUrl);
  recent_tab_helper()->PrimaryMainDocumentElementAvailable();
  FastForwardSnapshotController();
  ASSERT_EQ(0U, GetAllPages().size());

  const ClientId client_id = NewDownloadClientId();
  recent_tab_helper()->ObserveAndDownloadCurrentPage(client_id, 153L, "");
  RunUntilIdle();
  ASSERT_EQ(1U, GetAllPages().size());
  const OfflinePageItem& page = GetAllPages()[0];
  EXPECT_EQ(kTestUrl, page.url);
  EXPECT_EQ(client_id, page.client_id);
  EXPECT_EQ(153L, page.offline_id);

  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  EXPECT_EQ(2U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
}

// Simulates a download request to offline the current page made after loading
// is completed. Should end up with one offline page.
TEST_F(RecentTabHelperTest, DownloadRequestAfterFullyLoad) {
  const GURL kTestUrl("http://mystery.site/foo.html");
  NavigateAndCommit(kTestUrl);
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  ASSERT_EQ(0U, GetAllPages().size());

  const ClientId client_id = NewDownloadClientId();
  recent_tab_helper()->ObserveAndDownloadCurrentPage(client_id, 153L, "");
  RunUntilIdle();
  ASSERT_EQ(1U, GetAllPages().size());
  const OfflinePageItem& page = GetAllPages()[0];
  EXPECT_EQ(kTestUrl, page.url);
  EXPECT_EQ(client_id, page.client_id);
  EXPECT_EQ(153L, page.offline_id);
  EXPECT_EQ("", page.request_origin);
}

// Simulates a download request to offline the current page made after loading
// is completed. Should end up with one offline page.
TEST_F(RecentTabHelperTest, DownloadRequestAfterFullyLoadWithOrigin) {
  const GURL kTestUrl("http://mystery.site/foo.html");
  NavigateAndCommit(kTestUrl);
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  ASSERT_EQ(0U, GetAllPages().size());

  const ClientId client_id = NewDownloadClientId();
  recent_tab_helper()->ObserveAndDownloadCurrentPage(client_id, 153L, "abc");
  RunUntilIdle();
  ASSERT_EQ(1U, GetAllPages().size());
  const OfflinePageItem& page = GetAllPages()[0];
  EXPECT_EQ(kTestUrl, page.url);
  EXPECT_EQ(client_id, page.client_id);
  EXPECT_EQ(153L, page.offline_id);
  EXPECT_EQ("abc", page.request_origin);
}

// Simulates requests coming from last_n and downloads at the same time for a
// fully loaded page.
TEST_F(RecentTabHelperTest, SimultaneousCapturesFromLastNAndDownloads) {
  const GURL kTestUrl("http://mystery.site/foo.html");
  NavigateAndCommit(kTestUrl);
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  const int64_t download_offline_id = 153L;
  const ClientId download_client_id = NewDownloadClientId();
  recent_tab_helper()->ObserveAndDownloadCurrentPage(download_client_id,
                                                     download_offline_id, "");
  RunUntilIdle();
  ASSERT_EQ(2U, GetAllPages().size());

  const OfflinePageItem* downloads_page =
      FindPageForOfflineId(download_offline_id);
  ASSERT_TRUE(downloads_page);
  EXPECT_EQ(kTestUrl, downloads_page->url);
  EXPECT_EQ(download_client_id, downloads_page->client_id);

  const OfflinePageItem& last_n_page =
      GetAllPages()[0].offline_id != download_offline_id ? GetAllPages()[0]
                                                         : GetAllPages()[1];
  EXPECT_EQ(kTestUrl, last_n_page.url);
  EXPECT_EQ(kLastNNamespace, last_n_page.client_id.name_space);
}

// Simulates multiple tab hidden events -- triggers for last_n snapshots --
// happening at the same loading stages. The duplicate events should create new
// snapshots (so that dynamic pages are properly persisted; navigation/loading
// signals are poor signals for those).
TEST_F(RecentTabHelperTest, DuplicateTabHiddenEventsShouldTriggerNewSnapshots) {
  NavigateAndCommit(GURL("http://mystery.site/foo.html"));
  recent_tab_helper()->PrimaryMainDocumentElementAvailable();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(1U, page_added_count());
  EXPECT_EQ(0U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());

  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(2U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());

  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(3U, page_added_count());
  EXPECT_EQ(2U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());

  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(4U, page_added_count());
  EXPECT_EQ(3U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
}

// Simulates multiple download requests and verifies that overlapping requests
// are ignored.
TEST_F(RecentTabHelperTest, OverlappingDownloadRequestsAreIgnored) {
  // Navigates and commits then make two download snapshot requests.
  NavigateAndCommit(GURL("http://mystery.site/foo.html"));
  const ClientId client_id_1 = NewDownloadClientId();
  const int64_t offline_id_1 = 153L;
  recent_tab_helper()->ObserveAndDownloadCurrentPage(client_id_1, offline_id_1,
                                                     "");
  recent_tab_helper()->ObserveAndDownloadCurrentPage(NewDownloadClientId(),
                                                     351L, "");

  // Finish loading the page. Only the first request should be executed.
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  EXPECT_EQ(1U, page_added_count());
  EXPECT_EQ(0U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
  const OfflinePageItem& fist_page = GetAllPages()[0];
  EXPECT_EQ(client_id_1, fist_page.client_id);
  EXPECT_EQ(offline_id_1, fist_page.offline_id);

  // Make two additional download snapshot requests. Again only the first should
  // generate a snapshot.
  const ClientId client_id_3 = NewDownloadClientId();
  const int64_t offline_id_3 = 789L;
  recent_tab_helper()->ObserveAndDownloadCurrentPage(client_id_3, offline_id_3,
                                                     "");
  recent_tab_helper()->ObserveAndDownloadCurrentPage(NewDownloadClientId(),
                                                     987L, "");
  RunUntilIdle();
  EXPECT_EQ(2U, page_added_count());
  EXPECT_EQ(0U, model_removed_count());
  ASSERT_EQ(2U, GetAllPages().size());
  const OfflinePageItem* second_page = FindPageForOfflineId(offline_id_3);
  ASSERT_TRUE(second_page);
  EXPECT_EQ(client_id_3, second_page->client_id);
  EXPECT_EQ(offline_id_3, second_page->offline_id);
}

// Simulates a same document navigation and checks we snapshot correctly with
// last_n and downloads.
TEST_F(RecentTabHelperTest, SaveSameDocumentNavigationSnapshots) {
  // Navigates and load fully then hide the tab so that a snapshot is created.
  const GURL kTestUrl("http://mystery.site/foo.html");
  NavigateAndCommit(kTestUrl);
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(1U, page_added_count());
  EXPECT_EQ(0U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());

  // Now navigates same page and check the results of hiding the tab again.
  // Another snapshot should be created to the updated URL.
  const GURL kTestPageUrlWithFragment(kTestUrl.spec() + "#aaa");
  NavigateAndCommit(kTestPageUrlWithFragment);
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(2U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
  EXPECT_EQ(kTestPageUrlWithFragment, GetAllPages()[0].url);

  // Now create a download request and check the snapshot is properly created.
  const ClientId client_id = NewDownloadClientId();
  const int64_t offline_id = 153L;
  recent_tab_helper()->ObserveAndDownloadCurrentPage(client_id, offline_id, "");
  RunUntilIdle();
  EXPECT_EQ(3U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(2U, GetAllPages().size());
  const OfflinePageItem* downloads_page = FindPageForOfflineId(offline_id);
  EXPECT_EQ(kTestPageUrlWithFragment, downloads_page->url);
  EXPECT_EQ(client_id, downloads_page->client_id);
  EXPECT_EQ(offline_id, downloads_page->offline_id);
}

// Tests that a page reloaded is tracked as an actual load and properly saved.
TEST_F(RecentTabHelperTest, ReloadIsTrackedAsNavigationAndSavedOnlyUponLoad) {
  // Navigates and load fully then hide the tab so that a snapshot is created.
  NavigateAndCommit(GURL("http://mystery.site/foo.html"));
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  ASSERT_EQ(1U, GetAllPages().size());

  // Starts a reload and hides the tab before it minimally load. The previous
  // snapshot should be removed.
  Reload();

  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(1U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(0U, GetAllPages().size());

  // Finish loading and hide the tab. A new snapshot should be created.
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(2U, page_added_count());
  EXPECT_EQ(1U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
}

// Checks that a closing tab doesn't trigger the creation of a snapshot. And
// also that if the closure is reverted, a snapshot is saved upon the next hide
// event.
TEST_F(RecentTabHelperTest, NoSaveIfTabIsClosing) {
  // Navigates and fully load then close and hide the tab. No snapshots are
  // expected.
  NavigateAndCommit(GURL("http://mystery.site/foo.html"));
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  // Note: These two next calls are always expected to happen in this order.
  recent_tab_helper()->WillCloseTab();
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(0U, page_added_count());
  EXPECT_EQ(0U, model_removed_count());
  ASSERT_EQ(0U, GetAllPages().size());

  // Simulates the page being restored and shown again, then hidden. At this
  // moment a snapshot should be created.
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::VISIBLE);
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(1U, page_added_count());
  EXPECT_EQ(0U, model_removed_count());
  ASSERT_EQ(1U, GetAllPages().size());
}

TEST_F(RecentTabHelperTest, NoSaveOfflinePageCacheForPost) {
  // Navigate and finish loading, then move the snapshot controller's time
  // forward so it gets past timeouts. Nothing should be saved.
  NavigateAndCommitPost(GURL("http://mystery.site/foo.html"));
  recent_tab_helper()->DocumentOnLoadCompletedInPrimaryMainFrame();
  FastForwardSnapshotController();
  ASSERT_EQ(0U, GetAllPages().size());

  // Tab is hidden with a fully loaded page. A snapshot save should not happen
  // due to the POST method - OfflinePageCache is disabled.
  recent_tab_helper()->OnVisibilityChanged(content::Visibility::HIDDEN);
  RunUntilIdle();
  EXPECT_EQ(0U, page_added_count());
  ASSERT_EQ(0U, GetAllPages().size());

  // A manual download should succeed despite being ineligible for OPC.
  recent_tab_helper()->ObserveAndDownloadCurrentPage(NewDownloadClientId(),
                                                     123L, "");
  RunUntilIdle();
  EXPECT_EQ(1U, page_added_count());
  ASSERT_EQ(1U, GetAllPages().size());
}

class RecentTabHelperFencedFrameTest : public RecentTabHelperTest {
 public:
  RecentTabHelperFencedFrameTest() {
    scoped_feature_list_.InitAndEnableFeatureWithParameters(
        blink::features::kFencedFrames, {{"implementation_type", "mparch"}});
  }
  ~RecentTabHelperFencedFrameTest() override = default;

  content::RenderFrameHost* CreateFencedFrame(
      content::RenderFrameHost* parent) {
    content::RenderFrameHost* fenced_frame =
        content::RenderFrameHostTester::For(parent)->AppendFencedFrame();
    return fenced_frame;
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests that FencedFrame does not change the current page quality via resetting
// the snapshot controller.
TEST_F(RecentTabHelperFencedFrameTest, FencedFrameDoesNotChangePageQuality) {
  // Navigate and finish loading, then move the snapshot controller's time
  // forward so it sets the current page quality to FAIR_AND_IMPROVING.
  NavigateAndCommitPost(GURL("http://mystery.site/foo.html"));

  recent_tab_helper()->PrimaryMainDocumentElementAvailable();
  FastForwardSnapshotController();

  EXPECT_EQ(recent_tab_helper()->snapshot_controller_->current_page_quality(),
            SnapshotController::PageQuality::FAIR_AND_IMPROVING);

  // Create a fenced frame.
  content::RenderFrameHostTester::For(main_rfh())
      ->InitializeRenderFrameIfNeeded();
  content::RenderFrameHost* fenced_frame_rfh = CreateFencedFrame(main_rfh());
  GURL kFencedFrameUrl("https://fencedframe.com");
  std::unique_ptr<content::NavigationSimulator> navigation_simulator =
      content::NavigationSimulator::CreateRendererInitiated(kFencedFrameUrl,
                                                            fenced_frame_rfh);
  navigation_simulator->Commit();
  EXPECT_TRUE(fenced_frame_rfh->IsFencedFrameRoot());

  // Navigating the fenced frame to the fenced frame url should not change the
  // current page quality to POOR.
  EXPECT_EQ(recent_tab_helper()->snapshot_controller_->current_page_quality(),
            SnapshotController::PageQuality::FAIR_AND_IMPROVING);
}

}  // namespace offline_pages