Home | History | Annotate | Download | only in core
      1 // Copyright 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include <algorithm>
      6 #include <map>
      7 #include <string>
      8 #include <vector>
      9 
     10 #include "base/bind.h"
     11 #include "base/bind_helpers.h"
     12 #include "base/location.h"
     13 #include "base/memory/scoped_ptr.h"
     14 #include "base/message_loop/message_loop.h"
     15 #include "base/path_service.h"
     16 #include "base/strings/string_number_conversions.h"
     17 #include "base/values.h"
     18 #include "components/dom_distiller/core/article_distillation_update.h"
     19 #include "components/dom_distiller/core/distiller.h"
     20 #include "components/dom_distiller/core/distiller_page.h"
     21 #include "components/dom_distiller/core/fake_distiller_page.h"
     22 #include "components/dom_distiller/core/proto/distilled_article.pb.h"
     23 #include "components/dom_distiller/core/proto/distilled_page.pb.h"
     24 #include "net/url_request/url_request_context_getter.h"
     25 #include "testing/gmock/include/gmock/gmock.h"
     26 #include "testing/gtest/include/gtest/gtest.h"
     27 #include "third_party/dom_distiller_js/dom_distiller.pb.h"
     28 #include "third_party/dom_distiller_js/dom_distiller_json_converter.h"
     29 #include "ui/base/resource/resource_bundle.h"
     30 
     31 using std::vector;
     32 using std::string;
     33 using ::testing::Invoke;
     34 using ::testing::Return;
     35 using ::testing::_;
     36 
     37 using dom_distiller::proto::DomDistillerOptions;
     38 using dom_distiller::proto::DomDistillerResult;
     39 
     40 namespace {
     41 const char kTitle[] = "Title";
     42 const char kContent[] = "Content";
     43 const char kURL[] = "http://a.com/";
     44 const size_t kTotalImages = 2;
     45 const char* kImageURLs[kTotalImages] = {"http://a.com/img1.jpg",
     46                                         "http://a.com/img2.jpg"};
     47 const char* kImageData[kTotalImages] = {"abcde", "12345"};
     48 
     49 const string GetImageName(int page_num, int image_num) {
     50   return base::IntToString(page_num) + "_" + base::IntToString(image_num);
     51 }
     52 
     53 scoped_ptr<base::Value> CreateDistilledValueReturnedFromJS(
     54     const string& title,
     55     const string& content,
     56     const vector<int>& image_indices,
     57     const string& next_page_url,
     58     const string& prev_page_url = "") {
     59   DomDistillerResult result;
     60   result.set_title(title);
     61   result.mutable_distilled_content()->set_html(content);
     62   result.mutable_pagination_info()->set_next_page(next_page_url);
     63   result.mutable_pagination_info()->set_prev_page(prev_page_url);
     64 
     65   for (size_t i = 0; i < image_indices.size(); ++i) {
     66     result.add_image_urls(kImageURLs[image_indices[i]]);
     67   }
     68 
     69   return dom_distiller::proto::json::DomDistillerResult::WriteToValue(result);
     70 }
     71 
     72 // Return the sequence in which Distiller will distill pages.
     73 // Note: ignores any delays due to fetching images etc.
     74 vector<int> GetPagesInSequence(int start_page_num, int num_pages) {
     75   // Distiller prefers distilling past pages first. E.g. when distillation
     76   // starts on page 2 then pages are distilled in the order: 2, 1, 0, 3, 4.
     77   vector<int> page_nums;
     78   for (int page = start_page_num; page >= 0; --page)
     79     page_nums.push_back(page);
     80   for (int page = start_page_num + 1; page < num_pages; ++page)
     81     page_nums.push_back(page);
     82   return page_nums;
     83 }
     84 
     85 struct MultipageDistillerData {
     86  public:
     87   MultipageDistillerData() {}
     88   ~MultipageDistillerData() {}
     89   vector<string> page_urls;
     90   vector<string> content;
     91   vector<vector<int> > image_ids;
     92   // The Javascript values returned by mock distiller.
     93   ScopedVector<base::Value> distilled_values;
     94 
     95  private:
     96   DISALLOW_COPY_AND_ASSIGN(MultipageDistillerData);
     97 };
     98 
     99 void VerifyIncrementalUpdatesMatch(
    100     const MultipageDistillerData* distiller_data,
    101     int num_pages_in_article,
    102     const vector<dom_distiller::ArticleDistillationUpdate>& incremental_updates,
    103     int start_page_num) {
    104   vector<int> page_seq =
    105       GetPagesInSequence(start_page_num, num_pages_in_article);
    106   // Updates should contain a list of pages. Pages in an update should be in
    107   // the correct ascending page order regardless of |start_page_num|.
    108   // E.g. if distillation starts at page 2 of a 3 page article, the updates
    109   // will be [[2], [1, 2], [1, 2, 3]]. This example assumes that image fetches
    110   // do not delay distillation of a page. There can be scenarios when image
    111   // fetch delays distillation of a page (E.g. 1 is delayed due to image
    112   // fetches so updates can be in this order [[2], [2,3], [1,2,3]].
    113   for (size_t update_count = 0; update_count < incremental_updates.size();
    114        ++update_count) {
    115     const dom_distiller::ArticleDistillationUpdate& update =
    116         incremental_updates[update_count];
    117     EXPECT_EQ(update_count + 1, update.GetPagesSize());
    118 
    119     vector<int> expected_page_nums_in_update(
    120         page_seq.begin(), page_seq.begin() + update.GetPagesSize());
    121     std::sort(expected_page_nums_in_update.begin(),
    122               expected_page_nums_in_update.end());
    123 
    124     // If we already got the first page then there is no previous page.
    125     EXPECT_EQ((expected_page_nums_in_update[0] != 0), update.HasPrevPage());
    126 
    127     // if we already got the last page then there is no next page.
    128     EXPECT_EQ(
    129         (*expected_page_nums_in_update.rbegin()) != num_pages_in_article - 1,
    130         update.HasNextPage());
    131     for (size_t j = 0; j < update.GetPagesSize(); ++j) {
    132       int actual_page_num = expected_page_nums_in_update[j];
    133       EXPECT_EQ(distiller_data->page_urls[actual_page_num],
    134                 update.GetDistilledPage(j).url());
    135       EXPECT_EQ(distiller_data->content[actual_page_num],
    136                 update.GetDistilledPage(j).html());
    137     }
    138   }
    139 }
    140 
    141 scoped_ptr<MultipageDistillerData> CreateMultipageDistillerDataWithoutImages(
    142     size_t pages_size) {
    143   scoped_ptr<MultipageDistillerData> result(new MultipageDistillerData());
    144   string url_prefix = "http://a.com/";
    145   for (size_t page_num = 0; page_num < pages_size; ++page_num) {
    146     result->page_urls.push_back(url_prefix + base::IntToString(page_num));
    147     result->content.push_back("Content for page:" +
    148                               base::IntToString(page_num));
    149     result->image_ids.push_back(vector<int>());
    150     string next_page_url = (page_num + 1 < pages_size)
    151                                ? url_prefix + base::IntToString(page_num + 1)
    152                                : "";
    153     string prev_page_url =
    154         (page_num > 0) ? result->page_urls[page_num - 1] : "";
    155     scoped_ptr<base::Value> distilled_value =
    156         CreateDistilledValueReturnedFromJS(kTitle,
    157                                            result->content[page_num],
    158                                            result->image_ids[page_num],
    159                                            next_page_url,
    160                                            prev_page_url);
    161     result->distilled_values.push_back(distilled_value.release());
    162   }
    163   return result.Pass();
    164 }
    165 
    166 void VerifyArticleProtoMatchesMultipageData(
    167     const dom_distiller::DistilledArticleProto* article_proto,
    168     const MultipageDistillerData* distiller_data,
    169     size_t pages_size) {
    170   EXPECT_EQ(pages_size, static_cast<size_t>(article_proto->pages_size()));
    171   EXPECT_EQ(kTitle, article_proto->title());
    172   for (size_t page_num = 0; page_num < pages_size; ++page_num) {
    173     const dom_distiller::DistilledPageProto& page =
    174         article_proto->pages(page_num);
    175     EXPECT_EQ(distiller_data->content[page_num], page.html());
    176     EXPECT_EQ(distiller_data->page_urls[page_num], page.url());
    177     EXPECT_EQ(distiller_data->image_ids[page_num].size(),
    178               static_cast<size_t>(page.image_size()));
    179     const vector<int>& image_ids_for_page = distiller_data->image_ids[page_num];
    180     for (size_t img_num = 0; img_num < image_ids_for_page.size(); ++img_num) {
    181       EXPECT_EQ(kImageData[image_ids_for_page[img_num]],
    182                 page.image(img_num).data());
    183       EXPECT_EQ(GetImageName(page_num + 1, img_num),
    184                 page.image(img_num).name());
    185     }
    186   }
    187 }
    188 
    189 void AddComponentsResources() {
    190   base::FilePath pak_file;
    191   base::FilePath pak_dir;
    192   PathService::Get(base::DIR_MODULE, &pak_dir);
    193   pak_file = pak_dir.Append(FILE_PATH_LITERAL("components_resources.pak"));
    194   ui::ResourceBundle::GetSharedInstance().AddDataPackFromPath(
    195       pak_file, ui::SCALE_FACTOR_NONE);
    196 }
    197 
    198 }  // namespace
    199 
    200 namespace dom_distiller {
    201 
    202 using test::MockDistillerPage;
    203 using test::MockDistillerPageFactory;
    204 
    205 class TestDistillerURLFetcher : public DistillerURLFetcher {
    206  public:
    207   explicit TestDistillerURLFetcher(bool delay_fetch)
    208       : DistillerURLFetcher(NULL), delay_fetch_(delay_fetch) {
    209     responses_[kImageURLs[0]] = string(kImageData[0]);
    210     responses_[kImageURLs[1]] = string(kImageData[1]);
    211   }
    212 
    213   virtual void FetchURL(const string& url,
    214                         const URLFetcherCallback& callback) OVERRIDE {
    215     ASSERT_FALSE(callback.is_null());
    216     url_ = url;
    217     callback_ = callback;
    218     if (!delay_fetch_) {
    219       PostCallbackTask();
    220     }
    221   }
    222 
    223   void PostCallbackTask() {
    224     ASSERT_TRUE(base::MessageLoop::current());
    225     ASSERT_FALSE(callback_.is_null());
    226     base::MessageLoop::current()->PostTask(
    227         FROM_HERE, base::Bind(callback_, responses_[url_]));
    228   }
    229 
    230  private:
    231   std::map<string, string> responses_;
    232   string url_;
    233   URLFetcherCallback callback_;
    234   bool delay_fetch_;
    235 };
    236 
    237 class TestDistillerURLFetcherFactory : public DistillerURLFetcherFactory {
    238  public:
    239   TestDistillerURLFetcherFactory() : DistillerURLFetcherFactory(NULL) {}
    240 
    241   virtual ~TestDistillerURLFetcherFactory() {}
    242   virtual DistillerURLFetcher* CreateDistillerURLFetcher() const OVERRIDE {
    243     return new TestDistillerURLFetcher(false);
    244   }
    245 };
    246 
    247 class MockDistillerURLFetcherFactory : public DistillerURLFetcherFactory {
    248  public:
    249   MockDistillerURLFetcherFactory() : DistillerURLFetcherFactory(NULL) {}
    250   virtual ~MockDistillerURLFetcherFactory() {}
    251 
    252   MOCK_CONST_METHOD0(CreateDistillerURLFetcher, DistillerURLFetcher*());
    253 };
    254 
    255 class DistillerTest : public testing::Test {
    256  public:
    257   virtual ~DistillerTest() {}
    258 
    259   virtual void SetUp() OVERRIDE {
    260     AddComponentsResources();
    261   }
    262 
    263   void OnDistillArticleDone(scoped_ptr<DistilledArticleProto> proto) {
    264     article_proto_ = proto.Pass();
    265   }
    266 
    267   void OnDistillArticleUpdate(const ArticleDistillationUpdate& article_update) {
    268     in_sequence_updates_.push_back(article_update);
    269   }
    270 
    271   void DistillPage(const std::string& url,
    272                    scoped_ptr<DistillerPage> distiller_page) {
    273     distiller_->DistillPage(GURL(url),
    274                             distiller_page.Pass(),
    275                             base::Bind(&DistillerTest::OnDistillArticleDone,
    276                                        base::Unretained(this)),
    277                             base::Bind(&DistillerTest::OnDistillArticleUpdate,
    278                                        base::Unretained(this)));
    279   }
    280 
    281  protected:
    282   scoped_ptr<DistillerImpl> distiller_;
    283   scoped_ptr<DistilledArticleProto> article_proto_;
    284   std::vector<ArticleDistillationUpdate> in_sequence_updates_;
    285   MockDistillerPageFactory page_factory_;
    286   TestDistillerURLFetcherFactory url_fetcher_factory_;
    287 };
    288 
    289 ACTION_P3(DistillerPageOnDistillationDone, distiller_page, url, result) {
    290   distiller_page->OnDistillationDone(url, result);
    291 }
    292 
    293 scoped_ptr<DistillerPage> CreateMockDistillerPage(const base::Value* result,
    294                                                   const GURL& url) {
    295   MockDistillerPage* distiller_page = new MockDistillerPage();
    296   EXPECT_CALL(*distiller_page, DistillPageImpl(url, _))
    297       .WillOnce(DistillerPageOnDistillationDone(distiller_page, url, result));
    298   return scoped_ptr<DistillerPage>(distiller_page).Pass();
    299 }
    300 
    301 scoped_ptr<DistillerPage> CreateMockDistillerPageWithPendingJSCallback(
    302     MockDistillerPage** distiller_page_ptr,
    303     const GURL& url) {
    304   MockDistillerPage* distiller_page = new MockDistillerPage();
    305   *distiller_page_ptr = distiller_page;
    306   EXPECT_CALL(*distiller_page, DistillPageImpl(url, _));
    307   return scoped_ptr<DistillerPage>(distiller_page).Pass();
    308 }
    309 
    310 scoped_ptr<DistillerPage> CreateMockDistillerPages(
    311     MultipageDistillerData* distiller_data,
    312     size_t pages_size,
    313     int start_page_num) {
    314   MockDistillerPage* distiller_page = new MockDistillerPage();
    315   {
    316     testing::InSequence s;
    317     vector<int> page_nums = GetPagesInSequence(start_page_num, pages_size);
    318     for (size_t page_num = 0; page_num < pages_size; ++page_num) {
    319       int page = page_nums[page_num];
    320       GURL url = GURL(distiller_data->page_urls[page]);
    321       EXPECT_CALL(*distiller_page, DistillPageImpl(url, _))
    322           .WillOnce(DistillerPageOnDistillationDone(
    323               distiller_page, url, distiller_data->distilled_values[page]));
    324     }
    325   }
    326   return scoped_ptr<DistillerPage>(distiller_page).Pass();
    327 }
    328 
    329 TEST_F(DistillerTest, DistillPage) {
    330   base::MessageLoopForUI loop;
    331   scoped_ptr<base::Value> result =
    332       CreateDistilledValueReturnedFromJS(kTitle, kContent, vector<int>(), "");
    333   distiller_.reset(
    334       new DistillerImpl(url_fetcher_factory_, DomDistillerOptions()));
    335   DistillPage(kURL, CreateMockDistillerPage(result.get(), GURL(kURL)).Pass());
    336   base::MessageLoop::current()->RunUntilIdle();
    337   EXPECT_EQ(kTitle, article_proto_->title());
    338   EXPECT_EQ(article_proto_->pages_size(), 1);
    339   const DistilledPageProto& first_page = article_proto_->pages(0);
    340   EXPECT_EQ(kContent, first_page.html());
    341   EXPECT_EQ(kURL, first_page.url());
    342 }
    343 
    344 TEST_F(DistillerTest, DistillPageWithImages) {
    345   base::MessageLoopForUI loop;
    346   vector<int> image_indices;
    347   image_indices.push_back(0);
    348   image_indices.push_back(1);
    349   scoped_ptr<base::Value> result =
    350       CreateDistilledValueReturnedFromJS(kTitle, kContent, image_indices, "");
    351   distiller_.reset(
    352       new DistillerImpl(url_fetcher_factory_, DomDistillerOptions()));
    353   DistillPage(kURL, CreateMockDistillerPage(result.get(), GURL(kURL)).Pass());
    354   base::MessageLoop::current()->RunUntilIdle();
    355   EXPECT_EQ(kTitle, article_proto_->title());
    356   EXPECT_EQ(article_proto_->pages_size(), 1);
    357   const DistilledPageProto& first_page = article_proto_->pages(0);
    358   EXPECT_EQ(kContent, first_page.html());
    359   EXPECT_EQ(kURL, first_page.url());
    360   EXPECT_EQ(2, first_page.image_size());
    361   EXPECT_EQ(kImageData[0], first_page.image(0).data());
    362   EXPECT_EQ(GetImageName(1, 0), first_page.image(0).name());
    363   EXPECT_EQ(kImageData[1], first_page.image(1).data());
    364   EXPECT_EQ(GetImageName(1, 1), first_page.image(1).name());
    365 }
    366 
    367 TEST_F(DistillerTest, DistillMultiplePages) {
    368   base::MessageLoopForUI loop;
    369   const size_t kNumPages = 8;
    370   scoped_ptr<MultipageDistillerData> distiller_data =
    371       CreateMultipageDistillerDataWithoutImages(kNumPages);
    372 
    373   // Add images.
    374   int next_image_number = 0;
    375   for (size_t page_num = 0; page_num < kNumPages; ++page_num) {
    376     // Each page has different number of images.
    377     size_t tot_images = (page_num + kTotalImages) % (kTotalImages + 1);
    378     vector<int> image_indices;
    379     for (size_t img_num = 0; img_num < tot_images; img_num++) {
    380       image_indices.push_back(next_image_number);
    381       next_image_number = (next_image_number + 1) % kTotalImages;
    382     }
    383     distiller_data->image_ids.push_back(image_indices);
    384   }
    385 
    386   distiller_.reset(
    387       new DistillerImpl(url_fetcher_factory_, DomDistillerOptions()));
    388   DistillPage(
    389       distiller_data->page_urls[0],
    390       CreateMockDistillerPages(distiller_data.get(), kNumPages, 0).Pass());
    391   base::MessageLoop::current()->RunUntilIdle();
    392   VerifyArticleProtoMatchesMultipageData(
    393       article_proto_.get(), distiller_data.get(), kNumPages);
    394 }
    395 
    396 TEST_F(DistillerTest, DistillLinkLoop) {
    397   base::MessageLoopForUI loop;
    398   // Create a loop, the next page is same as the current page. This could
    399   // happen if javascript misparses a next page link.
    400   scoped_ptr<base::Value> result =
    401       CreateDistilledValueReturnedFromJS(kTitle, kContent, vector<int>(), kURL);
    402   distiller_.reset(
    403       new DistillerImpl(url_fetcher_factory_, DomDistillerOptions()));
    404   DistillPage(kURL, CreateMockDistillerPage(result.get(), GURL(kURL)).Pass());
    405   base::MessageLoop::current()->RunUntilIdle();
    406   EXPECT_EQ(kTitle, article_proto_->title());
    407   EXPECT_EQ(article_proto_->pages_size(), 1);
    408 }
    409 
    410 TEST_F(DistillerTest, CheckMaxPageLimitExtraPage) {
    411   base::MessageLoopForUI loop;
    412   const size_t kMaxPagesInArticle = 10;
    413   scoped_ptr<MultipageDistillerData> distiller_data =
    414       CreateMultipageDistillerDataWithoutImages(kMaxPagesInArticle);
    415 
    416   // Note: Next page url of the last page of article is set. So distiller will
    417   // try to do kMaxPagesInArticle + 1 calls if the max article limit does not
    418   // work.
    419   scoped_ptr<base::Value> last_page_data =
    420       CreateDistilledValueReturnedFromJS(
    421           kTitle,
    422           distiller_data->content[kMaxPagesInArticle - 1],
    423           vector<int>(),
    424           "",
    425           distiller_data->page_urls[kMaxPagesInArticle - 2]);
    426 
    427   distiller_data->distilled_values.pop_back();
    428   distiller_data->distilled_values.push_back(last_page_data.release());
    429 
    430   distiller_.reset(
    431       new DistillerImpl(url_fetcher_factory_, DomDistillerOptions()));
    432 
    433   distiller_->SetMaxNumPagesInArticle(kMaxPagesInArticle);
    434 
    435   DistillPage(distiller_data->page_urls[0],
    436               CreateMockDistillerPages(
    437                   distiller_data.get(), kMaxPagesInArticle, 0).Pass());
    438   base::MessageLoop::current()->RunUntilIdle();
    439   EXPECT_EQ(kTitle, article_proto_->title());
    440   EXPECT_EQ(kMaxPagesInArticle,
    441             static_cast<size_t>(article_proto_->pages_size()));
    442 }
    443 
    444 TEST_F(DistillerTest, CheckMaxPageLimitExactLimit) {
    445   base::MessageLoopForUI loop;
    446   const size_t kMaxPagesInArticle = 10;
    447   scoped_ptr<MultipageDistillerData> distiller_data =
    448       CreateMultipageDistillerDataWithoutImages(kMaxPagesInArticle);
    449 
    450   distiller_.reset(
    451       new DistillerImpl(url_fetcher_factory_, DomDistillerOptions()));
    452 
    453   // Check if distilling an article with exactly the page limit works.
    454   distiller_->SetMaxNumPagesInArticle(kMaxPagesInArticle);
    455 
    456   DistillPage(distiller_data->page_urls[0],
    457               CreateMockDistillerPages(
    458                   distiller_data.get(), kMaxPagesInArticle, 0).Pass());
    459   base::MessageLoop::current()->RunUntilIdle();
    460   EXPECT_EQ(kTitle, article_proto_->title());
    461   EXPECT_EQ(kMaxPagesInArticle,
    462             static_cast<size_t>(article_proto_->pages_size()));
    463 }
    464 
    465 TEST_F(DistillerTest, SinglePageDistillationFailure) {
    466   base::MessageLoopForUI loop;
    467   // To simulate failure return a null value.
    468   scoped_ptr<base::Value> nullValue(base::Value::CreateNullValue());
    469   distiller_.reset(
    470       new DistillerImpl(url_fetcher_factory_, DomDistillerOptions()));
    471   DistillPage(kURL,
    472               CreateMockDistillerPage(nullValue.get(), GURL(kURL)).Pass());
    473   base::MessageLoop::current()->RunUntilIdle();
    474   EXPECT_EQ("", article_proto_->title());
    475   EXPECT_EQ(0, article_proto_->pages_size());
    476 }
    477 
    478 TEST_F(DistillerTest, MultiplePagesDistillationFailure) {
    479   base::MessageLoopForUI loop;
    480   const size_t kNumPages = 8;
    481   scoped_ptr<MultipageDistillerData> distiller_data =
    482       CreateMultipageDistillerDataWithoutImages(kNumPages);
    483 
    484   // The page number of the failed page.
    485   size_t failed_page_num = 3;
    486   // reset distilled data of the failed page.
    487   distiller_data->distilled_values.erase(
    488       distiller_data->distilled_values.begin() + failed_page_num);
    489   distiller_data->distilled_values.insert(
    490       distiller_data->distilled_values.begin() + failed_page_num,
    491       base::Value::CreateNullValue());
    492   // Expect only calls till the failed page number.
    493   distiller_.reset(
    494       new DistillerImpl(url_fetcher_factory_, DomDistillerOptions()));
    495   DistillPage(distiller_data->page_urls[0],
    496               CreateMockDistillerPages(
    497                   distiller_data.get(), failed_page_num + 1, 0).Pass());
    498   base::MessageLoop::current()->RunUntilIdle();
    499   EXPECT_EQ(kTitle, article_proto_->title());
    500   VerifyArticleProtoMatchesMultipageData(
    501       article_proto_.get(), distiller_data.get(), failed_page_num);
    502 }
    503 
    504 TEST_F(DistillerTest, DistillPreviousPage) {
    505   base::MessageLoopForUI loop;
    506   const size_t kNumPages = 8;
    507 
    508   // The page number of the article on which distillation starts.
    509   int start_page_num = 3;
    510   scoped_ptr<MultipageDistillerData> distiller_data =
    511       CreateMultipageDistillerDataWithoutImages(kNumPages);
    512 
    513   distiller_.reset(
    514       new DistillerImpl(url_fetcher_factory_, DomDistillerOptions()));
    515   DistillPage(distiller_data->page_urls[start_page_num],
    516               CreateMockDistillerPages(
    517                   distiller_data.get(), kNumPages, start_page_num).Pass());
    518   base::MessageLoop::current()->RunUntilIdle();
    519   VerifyArticleProtoMatchesMultipageData(
    520       article_proto_.get(), distiller_data.get(), kNumPages);
    521 }
    522 
    523 TEST_F(DistillerTest, IncrementalUpdates) {
    524   base::MessageLoopForUI loop;
    525   const size_t kNumPages = 8;
    526 
    527   // The page number of the article on which distillation starts.
    528   int start_page_num = 3;
    529   scoped_ptr<MultipageDistillerData> distiller_data =
    530       CreateMultipageDistillerDataWithoutImages(kNumPages);
    531 
    532   distiller_.reset(
    533       new DistillerImpl(url_fetcher_factory_, DomDistillerOptions()));
    534   DistillPage(distiller_data->page_urls[start_page_num],
    535               CreateMockDistillerPages(
    536                   distiller_data.get(), kNumPages, start_page_num).Pass());
    537   base::MessageLoop::current()->RunUntilIdle();
    538   EXPECT_EQ(kTitle, article_proto_->title());
    539   EXPECT_EQ(kNumPages, static_cast<size_t>(article_proto_->pages_size()));
    540   EXPECT_EQ(kNumPages, in_sequence_updates_.size());
    541 
    542   VerifyIncrementalUpdatesMatch(
    543       distiller_data.get(), kNumPages, in_sequence_updates_, start_page_num);
    544 }
    545 
    546 TEST_F(DistillerTest, IncrementalUpdatesDoNotDeleteFinalArticle) {
    547   base::MessageLoopForUI loop;
    548   const size_t kNumPages = 8;
    549   int start_page_num = 3;
    550   scoped_ptr<MultipageDistillerData> distiller_data =
    551       CreateMultipageDistillerDataWithoutImages(kNumPages);
    552 
    553   distiller_.reset(
    554       new DistillerImpl(url_fetcher_factory_, DomDistillerOptions()));
    555   DistillPage(distiller_data->page_urls[start_page_num],
    556               CreateMockDistillerPages(
    557                   distiller_data.get(), kNumPages, start_page_num).Pass());
    558   base::MessageLoop::current()->RunUntilIdle();
    559   EXPECT_EQ(kNumPages, in_sequence_updates_.size());
    560 
    561   in_sequence_updates_.clear();
    562 
    563   // Should still be able to access article and pages.
    564   VerifyArticleProtoMatchesMultipageData(
    565       article_proto_.get(), distiller_data.get(), kNumPages);
    566 }
    567 
    568 TEST_F(DistillerTest, DeletingArticleDoesNotInterfereWithUpdates) {
    569   base::MessageLoopForUI loop;
    570   const size_t kNumPages = 8;
    571   scoped_ptr<MultipageDistillerData> distiller_data =
    572       CreateMultipageDistillerDataWithoutImages(kNumPages);
    573   // The page number of the article on which distillation starts.
    574   int start_page_num = 3;
    575 
    576   distiller_.reset(
    577       new DistillerImpl(url_fetcher_factory_, DomDistillerOptions()));
    578   DistillPage(distiller_data->page_urls[start_page_num],
    579               CreateMockDistillerPages(
    580                   distiller_data.get(), kNumPages, start_page_num).Pass());
    581   base::MessageLoop::current()->RunUntilIdle();
    582   EXPECT_EQ(kNumPages, in_sequence_updates_.size());
    583   EXPECT_EQ(kTitle, article_proto_->title());
    584   EXPECT_EQ(kNumPages, static_cast<size_t>(article_proto_->pages_size()));
    585 
    586   // Delete the article.
    587   article_proto_.reset();
    588   VerifyIncrementalUpdatesMatch(
    589       distiller_data.get(), kNumPages, in_sequence_updates_, start_page_num);
    590 }
    591 
    592 TEST_F(DistillerTest, CancelWithDelayedImageFetchCallback) {
    593   base::MessageLoopForUI loop;
    594   vector<int> image_indices;
    595   image_indices.push_back(0);
    596   scoped_ptr<base::Value> distilled_value =
    597       CreateDistilledValueReturnedFromJS(kTitle, kContent, image_indices, "");
    598   TestDistillerURLFetcher* delayed_fetcher = new TestDistillerURLFetcher(true);
    599   MockDistillerURLFetcherFactory mock_url_fetcher_factory;
    600   EXPECT_CALL(mock_url_fetcher_factory, CreateDistillerURLFetcher())
    601       .WillOnce(Return(delayed_fetcher));
    602   distiller_.reset(
    603       new DistillerImpl(mock_url_fetcher_factory, DomDistillerOptions()));
    604   DistillPage(
    605       kURL, CreateMockDistillerPage(distilled_value.get(), GURL(kURL)).Pass());
    606   base::MessageLoop::current()->RunUntilIdle();
    607 
    608   // Post callback from the url fetcher and then delete the distiller.
    609   delayed_fetcher->PostCallbackTask();
    610   distiller_.reset();
    611 
    612   base::MessageLoop::current()->RunUntilIdle();
    613 }
    614 
    615 TEST_F(DistillerTest, CancelWithDelayedJSCallback) {
    616   base::MessageLoopForUI loop;
    617   scoped_ptr<base::Value> distilled_value =
    618       CreateDistilledValueReturnedFromJS(kTitle, kContent, vector<int>(), "");
    619   MockDistillerPage* distiller_page = NULL;
    620   distiller_.reset(
    621       new DistillerImpl(url_fetcher_factory_, DomDistillerOptions()));
    622   DistillPage(kURL,
    623               CreateMockDistillerPageWithPendingJSCallback(&distiller_page,
    624                                                            GURL(kURL)));
    625   base::MessageLoop::current()->RunUntilIdle();
    626 
    627   ASSERT_TRUE(distiller_page);
    628   // Post the task to execute javascript and then delete the distiller.
    629   distiller_page->OnDistillationDone(GURL(kURL), distilled_value.get());
    630   distiller_.reset();
    631 
    632   base::MessageLoop::current()->RunUntilIdle();
    633 }
    634 
    635 }  // namespace dom_distiller
    636