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 "ash/public/cpp/shelf_model.h"
6
7 #include <set>
8 #include <string>
9
10 #include "ash/public/cpp/shelf_item_delegate.h"
11 #include "ash/public/cpp/shelf_model_observer.h"
12 #include "base/strings/stringprintf.h"
13 #include "testing/gtest/include/gtest/gtest.h"
14
15 namespace ash {
16
17 namespace {
18
19 // ShelfModelObserver implementation that tracks what message are invoked.
20 class TestShelfModelObserver : public ShelfModelObserver {
21 public:
22 TestShelfModelObserver() = default;
23
24 // Returns a string description of the changes that have occurred since this
25 // was last invoked. Resets state to initial state.
StateStringAndClear()26 std::string StateStringAndClear() {
27 std::string result;
28 AddToResult("added=%d", added_count_, &result);
29 AddToResult("removed=%d", removed_count_, &result);
30 AddToResult("changed=%d", changed_count_, &result);
31 AddToResult("moved=%d", moved_count_, &result);
32 AddToResult("delegate_changed=%d", delegate_changed_count_, &result);
33 added_count_ = removed_count_ = changed_count_ = moved_count_ =
34 delegate_changed_count_ = 0;
35 return result;
36 }
37
38 // ShelfModelObserver overrides:
ShelfItemAdded(int)39 void ShelfItemAdded(int) override { added_count_++; }
ShelfItemRemoved(int,const ShelfItem &)40 void ShelfItemRemoved(int, const ShelfItem&) override { removed_count_++; }
ShelfItemChanged(int,const ShelfItem &)41 void ShelfItemChanged(int, const ShelfItem&) override { changed_count_++; }
ShelfItemMoved(int,int)42 void ShelfItemMoved(int, int) override { moved_count_++; }
ShelfItemDelegateChanged(const ShelfID &,ShelfItemDelegate *,ShelfItemDelegate *)43 void ShelfItemDelegateChanged(const ShelfID&,
44 ShelfItemDelegate*,
45 ShelfItemDelegate*) override {
46 delegate_changed_count_++;
47 }
48
49 private:
AddToResult(const std::string & format,int count,std::string * result)50 void AddToResult(const std::string& format, int count, std::string* result) {
51 if (!count)
52 return;
53 if (!result->empty())
54 *result += " ";
55 *result += base::StringPrintf(format.c_str(), count);
56 }
57
58 int added_count_ = 0;
59 int removed_count_ = 0;
60 int changed_count_ = 0;
61 int moved_count_ = 0;
62 int delegate_changed_count_ = 0;
63
64 DISALLOW_COPY_AND_ASSIGN(TestShelfModelObserver);
65 };
66
67 class TestShelfItemDelegate : public ShelfItemDelegate {
68 public:
TestShelfItemDelegate(const ShelfID & shelf_id)69 TestShelfItemDelegate(const ShelfID& shelf_id)
70 : ShelfItemDelegate(shelf_id) {}
71
ItemSelected(std::unique_ptr<ui::Event> event,int64_t display_id,ash::ShelfLaunchSource source,ItemSelectedCallback callback,const ItemFilterPredicate & filter_predicate)72 void ItemSelected(std::unique_ptr<ui::Event> event,
73 int64_t display_id,
74 ash::ShelfLaunchSource source,
75 ItemSelectedCallback callback,
76 const ItemFilterPredicate& filter_predicate) override {}
ExecuteCommand(bool from_context_menu,int64_t command_id,int32_t event_flags,int64_t display_id)77 void ExecuteCommand(bool from_context_menu,
78 int64_t command_id,
79 int32_t event_flags,
80 int64_t display_id) override {}
Close()81 void Close() override {}
82 };
83
84 } // namespace
85
86 class ShelfModelTest : public testing::Test {
87 public:
88 ShelfModelTest() = default;
89 ~ShelfModelTest() override = default;
90
SetUp()91 void SetUp() override {
92 model_.reset(new ShelfModel);
93 observer_.reset(new TestShelfModelObserver);
94 model_->AddObserver(observer_.get());
95 }
96
TearDown()97 void TearDown() override {
98 observer_.reset();
99 model_.reset();
100 }
101
102 std::unique_ptr<ShelfModel> model_;
103 std::unique_ptr<TestShelfModelObserver> observer_;
104
105 private:
106 DISALLOW_COPY_AND_ASSIGN(ShelfModelTest);
107 };
108
TEST_F(ShelfModelTest,BasicAssertions)109 TEST_F(ShelfModelTest, BasicAssertions) {
110 // Add an item.
111 ShelfItem item1;
112 item1.id = ShelfID("item1");
113 item1.type = TYPE_PINNED_APP;
114 int index = model_->Add(item1);
115 EXPECT_EQ(1, model_->item_count());
116 EXPECT_LE(0, model_->ItemIndexByID(item1.id));
117 EXPECT_NE(model_->items().end(), model_->ItemByID(item1.id));
118 EXPECT_EQ("added=1", observer_->StateStringAndClear());
119
120 // Change to a platform app item.
121 item1.type = TYPE_APP;
122 model_->Set(index, item1);
123 EXPECT_EQ(item1.id, model_->items()[index].id);
124 EXPECT_LE(0, model_->ItemIndexByID(item1.id));
125 EXPECT_NE(model_->items().end(), model_->ItemByID(item1.id));
126 EXPECT_EQ("changed=1", observer_->StateStringAndClear());
127 EXPECT_EQ(TYPE_APP, model_->items()[index].type);
128
129 // Remove the item.
130 model_->RemoveItemAt(index);
131 EXPECT_EQ(0, model_->item_count());
132 EXPECT_EQ(-1, model_->ItemIndexByID(item1.id));
133 EXPECT_EQ(model_->items().end(), model_->ItemByID(item1.id));
134 EXPECT_EQ("removed=1", observer_->StateStringAndClear());
135
136 // Add an app item.
137 ShelfItem item2;
138 item2.id = ShelfID("item2");
139 item2.type = TYPE_PINNED_APP;
140 index = model_->Add(item2);
141 EXPECT_EQ(1, model_->item_count());
142 EXPECT_LE(0, model_->ItemIndexByID(item2.id));
143 EXPECT_NE(model_->items().end(), model_->ItemByID(item2.id));
144 EXPECT_EQ("added=1", observer_->StateStringAndClear());
145
146 // Change the item type.
147 item2.type = TYPE_APP;
148 model_->Set(index, item2);
149 EXPECT_LE(0, model_->ItemIndexByID(item2.id));
150 EXPECT_NE(model_->items().end(), model_->ItemByID(item2.id));
151 EXPECT_EQ("changed=1", observer_->StateStringAndClear());
152 EXPECT_EQ(TYPE_APP, model_->items()[index].type);
153
154 // Add another item.
155 ShelfItem item3;
156 item3.id = ShelfID("item3");
157 item3.type = TYPE_PINNED_APP;
158 model_->Add(item3);
159 EXPECT_EQ(2, model_->item_count());
160 EXPECT_LE(0, model_->ItemIndexByID(item3.id));
161 EXPECT_NE(model_->items().end(), model_->ItemByID(item3.id));
162 EXPECT_EQ("added=1", observer_->StateStringAndClear());
163
164 // Move the second to the first.
165 model_->Move(1, 0);
166 EXPECT_EQ("moved=1", observer_->StateStringAndClear());
167
168 // And back.
169 model_->Move(0, 1);
170 EXPECT_EQ("moved=1", observer_->StateStringAndClear());
171
172 // Verifies all the items get unique ids.
173 std::set<ShelfID> ids;
174 for (int i = 0; i < model_->item_count(); ++i)
175 ids.insert(model_->items()[i].id);
176 EXPECT_EQ(model_->item_count(), static_cast<int>(ids.size()));
177 }
178
179 // Assertions around where items are added.
TEST_F(ShelfModelTest,AddIndices)180 TEST_F(ShelfModelTest, AddIndices) {
181 // Insert a browser shortcut, like Chrome does, it should be added at index 0.
182 ShelfItem browser_shortcut;
183 browser_shortcut.id = ShelfID("browser");
184 browser_shortcut.type = TYPE_BROWSER_SHORTCUT;
185 EXPECT_EQ(0, model_->Add(browser_shortcut));
186
187 // App items should be after the browser shortcut.
188 ShelfItem item;
189 item.type = TYPE_APP;
190 item.id = ShelfID("id1");
191 int platform_app_index1 = model_->Add(item);
192 EXPECT_EQ(1, platform_app_index1);
193
194 // Add another platform app item, it should follow first.
195 item.id = ShelfID("id2");
196 int platform_app_index2 = model_->Add(item);
197 EXPECT_EQ(2, platform_app_index2);
198
199 // TYPE_PINNED_APP priority is higher than TYPE_APP but same as
200 // TYPE_BROWSER_SHORTCUT. So TYPE_PINNED_APP is located after
201 // TYPE_BROWSER_SHORTCUT.
202 item.type = TYPE_PINNED_APP;
203 item.id = ShelfID("id3");
204 int app_shortcut_index1 = model_->Add(item);
205 EXPECT_EQ(1, app_shortcut_index1);
206
207 item.type = TYPE_PINNED_APP;
208 item.id = ShelfID("id4");
209 int app_shortcut_index2 = model_->Add(item);
210 EXPECT_EQ(2, app_shortcut_index2);
211
212 // Check that AddAt() figures out the correct indexes for app shortcuts.
213 // TYPE_PINNED_APP and TYPE_BROWSER_SHORTCUT has the same weight.
214 // So TYPE_PINNED_APP is located at index 0. And, TYPE_BROWSER_SHORTCUT is
215 // located at index 1.
216 item.type = TYPE_PINNED_APP;
217 item.id = ShelfID("id5");
218 int app_shortcut_index3 = model_->AddAt(0, item);
219 EXPECT_EQ(0, app_shortcut_index3);
220
221 item.type = TYPE_PINNED_APP;
222 item.id = ShelfID("id6");
223 int app_shortcut_index4 = model_->AddAt(5, item);
224 EXPECT_EQ(4, app_shortcut_index4);
225
226 item.type = TYPE_PINNED_APP;
227 item.id = ShelfID("id7");
228 int app_shortcut_index5 = model_->AddAt(1, item);
229 EXPECT_EQ(1, app_shortcut_index5);
230
231 // Check that AddAt() figures out the correct indexes for apps.
232 item.type = TYPE_APP;
233 item.id = ShelfID("id8");
234 int platform_app_index3 = model_->AddAt(2, item);
235 EXPECT_EQ(6, platform_app_index3);
236
237 item.type = TYPE_APP;
238 item.id = ShelfID("id9");
239 int platform_app_index4 = model_->AddAt(6, item);
240 EXPECT_EQ(6, platform_app_index4);
241
242 EXPECT_EQ(TYPE_BROWSER_SHORTCUT, model_->items()[2].type);
243 }
244
245 // Test that the indexes for the running applications are properly determined.
TEST_F(ShelfModelTest,FirstRunningAppIndex)246 TEST_F(ShelfModelTest, FirstRunningAppIndex) {
247 // Insert the browser shortcut at index 0 and check that the running
248 // application index would be behind it.
249 ShelfItem item;
250 item.id = ShelfID("browser");
251 item.type = TYPE_BROWSER_SHORTCUT;
252 EXPECT_EQ(0, model_->Add(item));
253 EXPECT_EQ(1, model_->FirstRunningAppIndex());
254
255 // Insert an application shortcut and make sure that the running application
256 // index would be behind it.
257 item.type = TYPE_PINNED_APP;
258 item.id = ShelfID("pinned app");
259 EXPECT_EQ(1, model_->Add(item));
260 EXPECT_EQ(2, model_->FirstRunningAppIndex());
261
262 // Insert a two app items and check the first running app index.
263 item.type = TYPE_APP;
264 item.id = ShelfID("app1");
265 EXPECT_EQ(2, model_->Add(item));
266 EXPECT_EQ(2, model_->FirstRunningAppIndex());
267 item.id = ShelfID("app2");
268 EXPECT_EQ(3, model_->Add(item));
269 EXPECT_EQ(2, model_->FirstRunningAppIndex());
270 }
271
272 // Test item reordering on type/weight (eg. pinning) changes. crbug.com/248769.
TEST_F(ShelfModelTest,ReorderOnTypeChanges)273 TEST_F(ShelfModelTest, ReorderOnTypeChanges) {
274 // Add three pinned items.
275 ShelfItem item1;
276 item1.type = TYPE_PINNED_APP;
277 item1.id = ShelfID("id1");
278 int app1_index = model_->Add(item1);
279 EXPECT_EQ(0, app1_index);
280
281 ShelfItem item2;
282 item2.type = TYPE_PINNED_APP;
283 item2.id = ShelfID("id2");
284 int app2_index = model_->Add(item2);
285 EXPECT_EQ(1, app2_index);
286
287 ShelfItem item3;
288 item3.type = TYPE_PINNED_APP;
289 item3.id = ShelfID("id3");
290 int app3_index = model_->Add(item3);
291 EXPECT_EQ(2, app3_index);
292
293 // Unpinning an item moves it behind the shortcuts.
294 EXPECT_EQ(item3.id, model_->items()[2].id);
295 item2.type = TYPE_APP;
296 model_->Set(app2_index, item2);
297 EXPECT_EQ(item2.id, model_->items()[2].id);
298 }
299
300 // Test getting the index of ShelfIDs as a check for item presence.
TEST_F(ShelfModelTest,ItemIndexByID)301 TEST_F(ShelfModelTest, ItemIndexByID) {
302 // Expect empty and unknown ids to return the invalid index -1.
303 EXPECT_EQ(-1, model_->ItemIndexByID(ShelfID()));
304 EXPECT_EQ(-1, model_->ItemIndexByID(ShelfID("foo")));
305 EXPECT_EQ(-1, model_->ItemIndexByID(ShelfID("foo", "bar")));
306
307 // Add an item and expect to get a valid index for its id.
308 ShelfItem item1;
309 item1.type = TYPE_PINNED_APP;
310 item1.id = ShelfID("app_id1", "launch_id1");
311 const int index1 = model_->Add(item1);
312 EXPECT_EQ(index1, model_->ItemIndexByID(item1.id));
313
314 // Add another item and expect to get another valid index for its id.
315 ShelfItem item2;
316 item2.type = TYPE_APP;
317 item2.id = ShelfID("app_id2", "launch_id2");
318 const int index2 = model_->Add(item2);
319 EXPECT_EQ(index2, model_->ItemIndexByID(item2.id));
320
321 // Removing the first item should yield an invalid index for that item.
322 model_->RemoveItemAt(index1);
323 EXPECT_EQ(-1, model_->ItemIndexByID(item1.id));
324 // The index of the second item should be decremented, but still valid.
325 EXPECT_EQ(index2 - 1, model_->ItemIndexByID(item2.id));
326 EXPECT_LE(0, model_->ItemIndexByID(item2.id));
327 }
328
329 // Test pinning and unpinning a closed app, and checking if it is pinned.
TEST_F(ShelfModelTest,ClosedAppPinning)330 TEST_F(ShelfModelTest, ClosedAppPinning) {
331 const std::string app_id("app_id");
332
333 // Check the initial state.
334 EXPECT_FALSE(model_->IsAppPinned(app_id));
335 EXPECT_EQ(0, model_->item_count());
336
337 // Pinning a previously unknown app should add an item.
338 model_->PinAppWithID(app_id);
339 EXPECT_TRUE(model_->IsAppPinned(app_id));
340 EXPECT_EQ(1, model_->item_count());
341 EXPECT_EQ(TYPE_PINNED_APP, model_->items()[0].type);
342 EXPECT_EQ(app_id, model_->items()[0].id.app_id);
343
344 // Pinning the same app id again should have no change.
345 model_->PinAppWithID(app_id);
346 EXPECT_TRUE(model_->IsAppPinned(app_id));
347 EXPECT_EQ(1, model_->item_count());
348 EXPECT_EQ(TYPE_PINNED_APP, model_->items()[0].type);
349 EXPECT_EQ(app_id, model_->items()[0].id.app_id);
350
351 // Unpinning the app should remove the item.
352 model_->UnpinAppWithID(app_id);
353 EXPECT_FALSE(model_->IsAppPinned(app_id));
354 EXPECT_EQ(0, model_->item_count());
355
356 // Unpinning the same app id again should have no change.
357 model_->UnpinAppWithID(app_id);
358 EXPECT_FALSE(model_->IsAppPinned(app_id));
359 EXPECT_EQ(0, model_->item_count());
360 }
361
362 // Test pinning and unpinning a running app, and checking if it is pinned.
TEST_F(ShelfModelTest,RunningAppPinning)363 TEST_F(ShelfModelTest, RunningAppPinning) {
364 const std::string app_id("app_id");
365
366 // Check the initial state.
367 EXPECT_FALSE(model_->IsAppPinned(app_id));
368 EXPECT_EQ(0, model_->item_count());
369
370 // Add an example running app.
371 ShelfItem item;
372 item.type = TYPE_APP;
373 item.status = STATUS_RUNNING;
374 item.id = ShelfID(app_id);
375 const int index = model_->Add(item);
376
377 // The item should be added but not pinned.
378 EXPECT_FALSE(model_->IsAppPinned(app_id));
379 EXPECT_EQ(1, model_->item_count());
380 EXPECT_EQ(TYPE_APP, model_->items()[index].type);
381 EXPECT_EQ(item.id, model_->items()[index].id);
382
383 // Pinning the item should just change its type.
384 model_->PinAppWithID(app_id);
385 EXPECT_TRUE(model_->IsAppPinned(app_id));
386 EXPECT_EQ(1, model_->item_count());
387 EXPECT_EQ(TYPE_PINNED_APP, model_->items()[index].type);
388 EXPECT_EQ(item.id, model_->items()[index].id);
389
390 // Pinning the same app id again should have no change.
391 model_->PinAppWithID(app_id);
392 EXPECT_TRUE(model_->IsAppPinned(app_id));
393 EXPECT_EQ(1, model_->item_count());
394 EXPECT_EQ(TYPE_PINNED_APP, model_->items()[index].type);
395 EXPECT_EQ(item.id, model_->items()[index].id);
396
397 // Unpinning the app should leave the item unpinnned but running.
398 model_->UnpinAppWithID(app_id);
399 EXPECT_FALSE(model_->IsAppPinned(app_id));
400 EXPECT_EQ(1, model_->item_count());
401 EXPECT_EQ(TYPE_APP, model_->items()[index].type);
402 EXPECT_EQ(item.id, model_->items()[index].id);
403
404 // Unpinning the same app id again should have no change.
405 model_->UnpinAppWithID(app_id);
406 EXPECT_FALSE(model_->IsAppPinned(app_id));
407 EXPECT_EQ(1, model_->item_count());
408 EXPECT_EQ(TYPE_APP, model_->items()[index].type);
409 EXPECT_EQ(item.id, model_->items()[index].id);
410 }
411
412 // Tests that apps are updated properly when notifications are added or removed.
TEST_F(ShelfModelTest,AddRemoveNotification)413 TEST_F(ShelfModelTest, AddRemoveNotification) {
414 const std::string app_id("app_id");
415
416 // Add an example running app.
417 ShelfItem item;
418 item.type = TYPE_APP;
419 item.status = STATUS_RUNNING;
420 item.id = ShelfID(app_id);
421 const int index = model_->Add(item);
422
423 EXPECT_FALSE(model_->items()[index].has_notification);
424
425 // Update to add a notification for the app.
426 model_->UpdateItemNotification(app_id, true /* has_badge */);
427 EXPECT_TRUE(model_->items()[index].has_notification);
428
429 // Update to remove the notification for the app.
430 model_->UpdateItemNotification(app_id, false /* has_badge */);
431 EXPECT_FALSE(model_->items()[index].has_notification);
432 }
433
434 // Test that RemoveItemAndTakeShelfItemDelegate has the same effect as
435 // RemoveItemAt and returns the correct delegate.
TEST_F(ShelfModelTest,RemoveItemAndTakeShelfItemDelegate)436 TEST_F(ShelfModelTest, RemoveItemAndTakeShelfItemDelegate) {
437 // Add an item.
438 ShelfItem item1;
439 item1.id = ShelfID("item1");
440 item1.type = TYPE_PINNED_APP;
441 model_->Add(item1);
442 EXPECT_EQ(1, model_->item_count());
443 EXPECT_LE(0, model_->ItemIndexByID(item1.id));
444 EXPECT_NE(model_->items().end(), model_->ItemByID(item1.id));
445 EXPECT_EQ("added=1", observer_->StateStringAndClear());
446
447 // Set item delegate.
448 auto* delegate = new TestShelfItemDelegate(item1.id);
449 model_->SetShelfItemDelegate(item1.id,
450 std::unique_ptr<ShelfItemDelegate>(delegate));
451 EXPECT_EQ("delegate_changed=1", observer_->StateStringAndClear());
452
453 // Remove the item.
454 auto taken_delegate = model_->RemoveItemAndTakeShelfItemDelegate(item1.id);
455 EXPECT_EQ(0, model_->item_count());
456 EXPECT_EQ(-1, model_->ItemIndexByID(item1.id));
457 EXPECT_EQ(model_->items().end(), model_->ItemByID(item1.id));
458 EXPECT_EQ("removed=1", observer_->StateStringAndClear());
459 EXPECT_EQ(delegate, taken_delegate.get());
460 }
461
462 } // namespace ash
463