1 // Copyright (c) 2012 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 "chrome/browser/download/download_item_model.h"
6
7 #include <vector>
8
9 #include "base/i18n/rtl.h"
10 #include "base/logging.h"
11 #include "base/message_loop/message_loop.h"
12 #include "base/strings/string16.h"
13 #include "base/strings/string_util.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "content/public/test/mock_download_item.h"
16 #include "extensions/common/extension.h"
17 #include "grit/generated_resources.h"
18 #include "testing/gmock/include/gmock/gmock.h"
19 #include "testing/gtest/include/gtest/gtest.h"
20 #include "ui/base/l10n/l10n_util.h"
21 #include "ui/base/resource/resource_bundle.h"
22 #include "ui/base/text/bytes_formatting.h"
23 #include "ui/gfx/font_list.h"
24 #include "ui/gfx/text_utils.h"
25
26 using content::DownloadItem;
27 using ::testing::Mock;
28 using ::testing::NiceMock;
29 using ::testing::Return;
30 using ::testing::ReturnRefOfCopy;
31 using ::testing::SetArgPointee;
32 using ::testing::_;
33
34 namespace {
35
36 // Create a char array that has as many elements as there are download
37 // interrupt reasons. We can then use that in a COMPILE_ASSERT to make sure
38 // that all the interrupt reason codes are accounted for. The reason codes are
39 // unfortunately sparse, making this necessary.
40 char kInterruptReasonCounter[] = {
41 0, // content::DOWNLOAD_INTERRUPT_REASON_NONE
42 #define INTERRUPT_REASON(name,value) 0,
43 #include "content/public/browser/download_interrupt_reason_values.h"
44 #undef INTERRUPT_REASON
45 };
46 const size_t kInterruptReasonCount = ARRAYSIZE_UNSAFE(kInterruptReasonCounter);
47
48 // Default target path for a mock download item in DownloadItemModelTest.
49 const base::FilePath::CharType kDefaultTargetFilePath[] =
50 FILE_PATH_LITERAL("/foo/bar/foo.bar");
51
52 const base::FilePath::CharType kDefaultDisplayFileName[] =
53 FILE_PATH_LITERAL("foo.bar");
54
55 // Default URL for a mock download item in DownloadItemModelTest.
56 const char kDefaultURL[] = "http://example.com/foo.bar";
57
58 class DownloadItemModelTest : public testing::Test {
59 public:
DownloadItemModelTest()60 DownloadItemModelTest()
61 : model_(&item_) {}
62
~DownloadItemModelTest()63 virtual ~DownloadItemModelTest() {
64 }
65
66 protected:
67 // Sets up defaults for the download item and sets |model_| to a new
68 // DownloadItemModel that uses the mock download item.
SetupDownloadItemDefaults()69 void SetupDownloadItemDefaults() {
70 ON_CALL(item_, GetReceivedBytes()).WillByDefault(Return(1));
71 ON_CALL(item_, GetTotalBytes()).WillByDefault(Return(2));
72 ON_CALL(item_, TimeRemaining(_)).WillByDefault(Return(false));
73 ON_CALL(item_, GetMimeType()).WillByDefault(Return("text/html"));
74 ON_CALL(item_, AllDataSaved()).WillByDefault(Return(false));
75 ON_CALL(item_, GetOpenWhenComplete()).WillByDefault(Return(false));
76 ON_CALL(item_, GetFileExternallyRemoved()).WillByDefault(Return(false));
77 ON_CALL(item_, GetState())
78 .WillByDefault(Return(DownloadItem::IN_PROGRESS));
79 ON_CALL(item_, GetURL())
80 .WillByDefault(ReturnRefOfCopy(GURL(kDefaultURL)));
81 ON_CALL(item_, GetFileNameToReportUser())
82 .WillByDefault(Return(base::FilePath(kDefaultDisplayFileName)));
83 ON_CALL(item_, GetTargetFilePath())
84 .WillByDefault(ReturnRefOfCopy(base::FilePath(kDefaultTargetFilePath)));
85 ON_CALL(item_, GetTargetDisposition())
86 .WillByDefault(
87 Return(DownloadItem::TARGET_DISPOSITION_OVERWRITE));
88 ON_CALL(item_, IsPaused()).WillByDefault(Return(false));
89 }
90
SetupInterruptedDownloadItem(content::DownloadInterruptReason reason)91 void SetupInterruptedDownloadItem(content::DownloadInterruptReason reason) {
92 EXPECT_CALL(item_, GetLastReason()).WillRepeatedly(Return(reason));
93 EXPECT_CALL(item_, GetState())
94 .WillRepeatedly(Return(
95 (reason == content::DOWNLOAD_INTERRUPT_REASON_NONE) ?
96 DownloadItem::IN_PROGRESS :
97 DownloadItem::INTERRUPTED));
98 }
99
item()100 content::MockDownloadItem& item() {
101 return item_;
102 }
103
model()104 DownloadItemModel& model() {
105 return model_;
106 }
107
108 private:
109 NiceMock<content::MockDownloadItem> item_;
110 DownloadItemModel model_;
111 };
112
113 } // namespace
114
TEST_F(DownloadItemModelTest,InterruptedStatus)115 TEST_F(DownloadItemModelTest, InterruptedStatus) {
116 // Test that we have the correct interrupt status message for downloads that
117 // are in the INTERRUPTED state.
118 const struct TestCase {
119 // The reason.
120 content::DownloadInterruptReason reason;
121
122 // Expected status string. This will include the progress as well.
123 const char* expected_status;
124 } kTestCases[] = {
125 { content::DOWNLOAD_INTERRUPT_REASON_NONE,
126 "1/2 B" },
127 { content::DOWNLOAD_INTERRUPT_REASON_FILE_FAILED,
128 "Failed - Download error" },
129 { content::DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED,
130 "Failed - Insufficient permissions" },
131 { content::DOWNLOAD_INTERRUPT_REASON_FILE_NO_SPACE,
132 "Failed - Disk full" },
133 { content::DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG,
134 "Failed - Path too long" },
135 { content::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE,
136 "Failed - File too large" },
137 { content::DOWNLOAD_INTERRUPT_REASON_FILE_VIRUS_INFECTED,
138 "Failed - Virus detected" },
139 { content::DOWNLOAD_INTERRUPT_REASON_FILE_BLOCKED,
140 "Failed - Blocked" },
141 { content::DOWNLOAD_INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED,
142 "Failed - Virus scan failed" },
143 { content::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_SHORT,
144 "Failed - File truncated" },
145 { content::DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR,
146 "Failed - System busy" },
147 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_FAILED,
148 "Failed - Network error" },
149 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_TIMEOUT,
150 "Failed - Network timeout" },
151 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED,
152 "Failed - Network disconnected" },
153 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_SERVER_DOWN,
154 "Failed - Server unavailable" },
155 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_FAILED,
156 "Failed - Server problem" },
157 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_NO_RANGE,
158 "Failed - Download error" },
159 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_PRECONDITION,
160 "Failed - Download error" },
161 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_BAD_CONTENT,
162 "Failed - No file" },
163 { content::DOWNLOAD_INTERRUPT_REASON_USER_CANCELED,
164 "Cancelled" },
165 { content::DOWNLOAD_INTERRUPT_REASON_USER_SHUTDOWN,
166 "Failed - Shutdown" },
167 { content::DOWNLOAD_INTERRUPT_REASON_CRASH,
168 "Failed - Crash" },
169 };
170 COMPILE_ASSERT(kInterruptReasonCount == ARRAYSIZE_UNSAFE(kTestCases),
171 interrupt_reason_mismatch);
172
173 SetupDownloadItemDefaults();
174 for (unsigned i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); ++i) {
175 const TestCase& test_case = kTestCases[i];
176 SetupInterruptedDownloadItem(test_case.reason);
177 EXPECT_STREQ(test_case.expected_status,
178 UTF16ToUTF8(model().GetStatusText()).c_str());
179 }
180 }
181
182 // Note: This test is currently skipped on Android. See http://crbug.com/139398
TEST_F(DownloadItemModelTest,InterruptTooltip)183 TEST_F(DownloadItemModelTest, InterruptTooltip) {
184 // Test that we have the correct interrupt tooltip for downloads that are in
185 // the INTERRUPTED state.
186 const struct TestCase {
187 // The reason.
188 content::DownloadInterruptReason reason;
189
190 // Expected tooltip text. The tooltip text for interrupted downloads
191 // typically consist of two lines. One for the filename and one for the
192 // interrupt reason. The returned string contains a newline.
193 const char* expected_tooltip;
194 } kTestCases[] = {
195 { content::DOWNLOAD_INTERRUPT_REASON_NONE,
196 "foo.bar" },
197 { content::DOWNLOAD_INTERRUPT_REASON_FILE_FAILED,
198 "foo.bar\nDownload error" },
199 { content::DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED,
200 "foo.bar\nInsufficient permissions" },
201 { content::DOWNLOAD_INTERRUPT_REASON_FILE_NO_SPACE,
202 "foo.bar\nDisk full" },
203 { content::DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG,
204 "foo.bar\nPath too long" },
205 { content::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE,
206 "foo.bar\nFile too large" },
207 { content::DOWNLOAD_INTERRUPT_REASON_FILE_VIRUS_INFECTED,
208 "foo.bar\nVirus detected" },
209 { content::DOWNLOAD_INTERRUPT_REASON_FILE_BLOCKED,
210 "foo.bar\nBlocked" },
211 { content::DOWNLOAD_INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED,
212 "foo.bar\nVirus scan failed" },
213 { content::DOWNLOAD_INTERRUPT_REASON_FILE_TOO_SHORT,
214 "foo.bar\nFile truncated" },
215 { content::DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR,
216 "foo.bar\nSystem busy" },
217 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_FAILED,
218 "foo.bar\nNetwork error" },
219 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_TIMEOUT,
220 "foo.bar\nNetwork timeout" },
221 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED,
222 "foo.bar\nNetwork disconnected" },
223 { content::DOWNLOAD_INTERRUPT_REASON_NETWORK_SERVER_DOWN,
224 "foo.bar\nServer unavailable" },
225 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_FAILED,
226 "foo.bar\nServer problem" },
227 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_NO_RANGE,
228 "foo.bar\nDownload error" },
229 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_PRECONDITION,
230 "foo.bar\nDownload error" },
231 { content::DOWNLOAD_INTERRUPT_REASON_SERVER_BAD_CONTENT,
232 "foo.bar\nNo file" },
233 { content::DOWNLOAD_INTERRUPT_REASON_USER_CANCELED,
234 "foo.bar" },
235 { content::DOWNLOAD_INTERRUPT_REASON_USER_SHUTDOWN,
236 "foo.bar\nShutdown" },
237 { content::DOWNLOAD_INTERRUPT_REASON_CRASH,
238 "foo.bar\nCrash" },
239 };
240 COMPILE_ASSERT(kInterruptReasonCount == ARRAYSIZE_UNSAFE(kTestCases),
241 interrupt_reason_mismatch);
242
243 // Large tooltip width. Should be large enough to accommodate the entire
244 // tooltip without truncation.
245 const int kLargeTooltipWidth = 1000;
246
247 // Small tooltip width. Small enough to require truncation of most
248 // tooltips. Used to test eliding logic.
249 const int kSmallTooltipWidth = 40;
250
251 const gfx::FontList& font_list =
252 ui::ResourceBundle::GetSharedInstance().GetFontList(
253 ui::ResourceBundle::BaseFont);
254 SetupDownloadItemDefaults();
255 for (unsigned i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); ++i) {
256 const TestCase& test_case = kTestCases[i];
257 SetupInterruptedDownloadItem(test_case.reason);
258
259 // GetTooltipText() elides the tooltip so that the text would fit within a
260 // given width. The following test would fail if kLargeTooltipWidth isn't
261 // large enough to accomodate all the strings.
262 EXPECT_STREQ(
263 test_case.expected_tooltip,
264 UTF16ToUTF8(model().GetTooltipText(font_list,
265 kLargeTooltipWidth)).c_str());
266
267 // Check that if the width is small, the returned tooltip only contains
268 // lines of the given width or smaller.
269 std::vector<base::string16> lines;
270 base::string16 truncated_tooltip =
271 model().GetTooltipText(font_list, kSmallTooltipWidth);
272 Tokenize(truncated_tooltip, ASCIIToUTF16("\n"), &lines);
273 for (unsigned i = 0; i < lines.size(); ++i)
274 EXPECT_GE(kSmallTooltipWidth, gfx::GetStringWidth(lines[i], font_list));
275 }
276 }
277
TEST_F(DownloadItemModelTest,InProgressStatus)278 TEST_F(DownloadItemModelTest, InProgressStatus) {
279 const struct TestCase {
280 int64 received_bytes; // Return value of GetReceivedBytes().
281 int64 total_bytes; // Return value of GetTotalBytes().
282 bool time_remaining_known; // If TimeRemaining() is known.
283 bool open_when_complete; // GetOpenWhenComplete().
284 bool is_paused; // IsPaused().
285 const char* expected_status; // Expected status text.
286 } kTestCases[] = {
287 // These are all the valid combinations of the above fields for a download
288 // that is in IN_PROGRESS state. Go through all of them and check the return
289 // value of DownloadItemModel::GetStatusText(). The point isn't to lock down
290 // the status strings, but to make sure we end up with something sane for
291 // all the circumstances we care about.
292 //
293 // For GetReceivedBytes()/GetTotalBytes(), we only check whether each is
294 // non-zero. In addition, if |total_bytes| is zero, then
295 // |time_remaining_known| is also false.
296 //
297 // .-- .TimeRemaining() is known.
298 // | .-- .GetOpenWhenComplete()
299 // | | .---- .IsPaused()
300 { 0, 0, false, false, false, "Starting..." },
301 { 1, 0, false, false, false, "1 B" },
302 { 0, 2, false, false, false, "Starting..." },
303 { 1, 2, false, false, false, "1/2 B" },
304 { 0, 2, true, false, false, "0/2 B, 10 secs left" },
305 { 1, 2, true, false, false, "1/2 B, 10 secs left" },
306 { 0, 0, false, true, false, "Opening when complete" },
307 { 1, 0, false, true, false, "Opening when complete" },
308 { 0, 2, false, true, false, "Opening when complete" },
309 { 1, 2, false, true, false, "Opening when complete" },
310 { 0, 2, true, true, false, "Opening in 10 secs..." },
311 { 1, 2, true, true, false, "Opening in 10 secs..." },
312 { 0, 0, false, false, true, "0 B, Paused" },
313 { 1, 0, false, false, true, "1 B, Paused" },
314 { 0, 2, false, false, true, "0/2 B, Paused" },
315 { 1, 2, false, false, true, "1/2 B, Paused" },
316 { 0, 2, true, false, true, "0/2 B, Paused" },
317 { 1, 2, true, false, true, "1/2 B, Paused" },
318 { 0, 0, false, true, true, "0 B, Paused" },
319 { 1, 0, false, true, true, "1 B, Paused" },
320 { 0, 2, false, true, true, "0/2 B, Paused" },
321 { 1, 2, false, true, true, "1/2 B, Paused" },
322 { 0, 2, true, true, true, "0/2 B, Paused" },
323 { 1, 2, true, true, true, "1/2 B, Paused" },
324 };
325
326 SetupDownloadItemDefaults();
327
328 for (unsigned i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); i++) {
329 const TestCase& test_case = kTestCases[i];
330 Mock::VerifyAndClearExpectations(&item());
331 Mock::VerifyAndClearExpectations(&model());
332 EXPECT_CALL(item(), GetReceivedBytes())
333 .WillRepeatedly(Return(test_case.received_bytes));
334 EXPECT_CALL(item(), GetTotalBytes())
335 .WillRepeatedly(Return(test_case.total_bytes));
336 EXPECT_CALL(item(), TimeRemaining(_))
337 .WillRepeatedly(testing::DoAll(
338 testing::SetArgPointee<0>(base::TimeDelta::FromSeconds(10)),
339 Return(test_case.time_remaining_known)));
340 EXPECT_CALL(item(), GetOpenWhenComplete())
341 .WillRepeatedly(Return(test_case.open_when_complete));
342 EXPECT_CALL(item(), IsPaused())
343 .WillRepeatedly(Return(test_case.is_paused));
344
345 EXPECT_STREQ(test_case.expected_status,
346 UTF16ToUTF8(model().GetStatusText()).c_str());
347 }
348 }
349
TEST_F(DownloadItemModelTest,ShouldShowInShelf)350 TEST_F(DownloadItemModelTest, ShouldShowInShelf) {
351 SetupDownloadItemDefaults();
352
353 // By default the download item should be displayable on the shelf.
354 EXPECT_TRUE(model().ShouldShowInShelf());
355
356 // Once explicitly set, ShouldShowInShelf() should return the explicit value.
357 model().SetShouldShowInShelf(false);
358 EXPECT_FALSE(model().ShouldShowInShelf());
359
360 model().SetShouldShowInShelf(true);
361 EXPECT_TRUE(model().ShouldShowInShelf());
362 }
363
TEST_F(DownloadItemModelTest,ShouldRemoveFromShelfWhenComplete)364 TEST_F(DownloadItemModelTest, ShouldRemoveFromShelfWhenComplete) {
365 const struct TestCase {
366 DownloadItem::DownloadState state;
367 bool is_dangerous; // Expectation for IsDangerous().
368 bool is_auto_open; // Expectation for GetOpenWhenComplete().
369 bool auto_opened; // Whether the download was successfully
370 // auto-opened. Expecation for GetAutoOpened().
371 bool expected_result;
372 } kTestCases[] = {
373 // All the valid combinations of state, is_dangerous, is_auto_open and
374 // auto_opened.
375 //
376 // .--- Is dangerous.
377 // | .--- Auto open or temporary.
378 // | | .--- Auto opened.
379 // | | | .--- Expected result.
380 { DownloadItem::IN_PROGRESS, false, false, false, false},
381 { DownloadItem::IN_PROGRESS, false, true , false, true },
382 { DownloadItem::IN_PROGRESS, true , false, false, false},
383 { DownloadItem::IN_PROGRESS, true , true , false, false},
384 { DownloadItem::COMPLETE, false, false, false, false},
385 { DownloadItem::COMPLETE, false, true , false, false},
386 { DownloadItem::COMPLETE, false, false, true , true },
387 { DownloadItem::COMPLETE, false, true , true , true },
388 { DownloadItem::CANCELLED, false, false, false, false},
389 { DownloadItem::CANCELLED, false, true , false, false},
390 { DownloadItem::CANCELLED, true , false, false, false},
391 { DownloadItem::CANCELLED, true , true , false, false},
392 { DownloadItem::INTERRUPTED, false, false, false, false},
393 { DownloadItem::INTERRUPTED, false, true , false, false},
394 { DownloadItem::INTERRUPTED, true , false, false, false},
395 { DownloadItem::INTERRUPTED, true , true , false, false}
396 };
397
398 SetupDownloadItemDefaults();
399
400 for (unsigned i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); i++) {
401 const TestCase& test_case = kTestCases[i];
402 EXPECT_CALL(item(), GetOpenWhenComplete())
403 .WillRepeatedly(Return(test_case.is_auto_open));
404 EXPECT_CALL(item(), GetState())
405 .WillRepeatedly(Return(test_case.state));
406 EXPECT_CALL(item(), IsDangerous())
407 .WillRepeatedly(Return(test_case.is_dangerous));
408 EXPECT_CALL(item(), GetAutoOpened())
409 .WillRepeatedly(Return(test_case.auto_opened));
410
411 EXPECT_EQ(test_case.expected_result,
412 model().ShouldRemoveFromShelfWhenComplete())
413 << "Test case: " << i;
414 Mock::VerifyAndClearExpectations(&item());
415 Mock::VerifyAndClearExpectations(&model());
416 }
417 }
418