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