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#import "ui/app_list/cocoa/apps_search_results_controller.h"
6
7#include "base/mac/scoped_nsobject.h"
8#include "base/message_loop/message_loop.h"
9#include "base/strings/stringprintf.h"
10#include "base/strings/sys_string_conversions.h"
11#include "base/strings/utf_string_conversions.h"
12#import "testing/gtest_mac.h"
13#include "ui/app_list/search_result.h"
14#include "ui/app_list/test/app_list_test_model.h"
15#include "ui/base/models/simple_menu_model.h"
16#import "ui/events/test/cocoa_test_event_utils.h"
17#include "ui/gfx/image/image_skia_util_mac.h"
18#import "ui/gfx/test/ui_cocoa_test_helper.h"
19
20@interface TestAppsSearchResultsDelegate : NSObject<AppsSearchResultsDelegate> {
21 @private
22  app_list::test::AppListTestModel appListModel_;
23  app_list::SearchResult* lastOpenedResult_;
24  int redoSearchCount_;
25}
26
27@property(readonly, nonatomic) app_list::SearchResult* lastOpenedResult;
28@property(readonly, nonatomic) int redoSearchCount;
29
30- (void)quitMessageLoop;
31
32@end
33
34@implementation TestAppsSearchResultsDelegate
35
36@synthesize lastOpenedResult = lastOpenedResult_;
37@synthesize redoSearchCount = redoSearchCount_;
38
39- (app_list::AppListModel*)appListModel {
40  return &appListModel_;
41}
42
43- (void)openResult:(app_list::SearchResult*)result {
44  lastOpenedResult_ = result;
45}
46
47- (void)redoSearch {
48  ++redoSearchCount_;
49}
50
51- (void)quitMessageLoop {
52  base::MessageLoop::current()->QuitNow();
53}
54
55@end
56
57namespace app_list {
58namespace test {
59namespace {
60
61const int kDefaultResultsCount = 3;
62
63class SearchResultWithMenu : public SearchResult {
64 public:
65  SearchResultWithMenu(const std::string& title, const std::string& details)
66      : menu_model_(NULL),
67        menu_ready_(true) {
68    set_title(base::ASCIIToUTF16(title));
69    set_details(base::ASCIIToUTF16(details));
70    menu_model_.AddItem(0, base::UTF8ToUTF16("Menu For: " + title));
71  }
72
73  void SetMenuReadyForTesting(bool ready) {
74    menu_ready_ = ready;
75  }
76
77  virtual ui::MenuModel* GetContextMenuModel() OVERRIDE {
78    if (!menu_ready_)
79      return NULL;
80
81    return &menu_model_;
82  }
83
84 private:
85  ui::SimpleMenuModel menu_model_;
86  bool menu_ready_;
87
88  DISALLOW_COPY_AND_ASSIGN(SearchResultWithMenu);
89};
90
91class AppsSearchResultsControllerTest : public ui::CocoaTest {
92 public:
93  AppsSearchResultsControllerTest() {}
94
95  void AddTestResultAtIndex(size_t index,
96                            const std::string& title,
97                            const std::string& details) {
98    scoped_ptr<SearchResult> result(new SearchResultWithMenu(title, details));
99    AppListModel::SearchResults* results = [delegate_ appListModel]->results();
100    results->AddAt(index, result.release());
101  }
102
103  SearchResult* ModelResultAt(size_t index) {
104    return [delegate_ appListModel]->results()->GetItemAt(index);
105  }
106
107  NSCell* ViewResultAt(NSInteger index) {
108    NSTableView* table_view = [apps_search_results_controller_ tableView];
109    return [table_view preparedCellAtColumn:0
110                                        row:index];
111  }
112
113  void SetMenuReadyAt(size_t index, bool ready) {
114    SearchResultWithMenu* result =
115        static_cast<SearchResultWithMenu*>(ModelResultAt(index));
116    result->SetMenuReadyForTesting(ready);
117  }
118
119  BOOL SimulateKeyAction(SEL c) {
120    return [apps_search_results_controller_ handleCommandBySelector:c];
121  }
122
123  void ExpectConsistent();
124
125  // ui::CocoaTest overrides:
126  virtual void SetUp() OVERRIDE;
127  virtual void TearDown() OVERRIDE;
128
129 protected:
130  base::scoped_nsobject<TestAppsSearchResultsDelegate> delegate_;
131  base::scoped_nsobject<AppsSearchResultsController>
132      apps_search_results_controller_;
133
134 private:
135  DISALLOW_COPY_AND_ASSIGN(AppsSearchResultsControllerTest);
136};
137
138void AppsSearchResultsControllerTest::ExpectConsistent() {
139  NSInteger item_count = [delegate_ appListModel]->results()->item_count();
140  ASSERT_EQ(item_count,
141            [[apps_search_results_controller_ tableView] numberOfRows]);
142
143  // Compare content strings to ensure the order of items is consistent, and any
144  // model data that should have been reloaded has been reloaded in the view.
145  for (NSInteger i = 0; i < item_count; ++i) {
146    SearchResult* result = ModelResultAt(i);
147    base::string16 string_in_model = result->title();
148    if (!result->details().empty())
149      string_in_model += base::ASCIIToUTF16("\n") + result->details();
150    EXPECT_NSEQ(base::SysUTF16ToNSString(string_in_model),
151                [[ViewResultAt(i) attributedStringValue] string]);
152  }
153}
154
155void AppsSearchResultsControllerTest::SetUp() {
156  apps_search_results_controller_.reset(
157      [[AppsSearchResultsController alloc] initWithAppsSearchResultsFrameSize:
158          NSMakeSize(400, 400)]);
159  // The view is initially hidden. Give it a non-zero height so it draws.
160  [[apps_search_results_controller_ view] setFrameSize:NSMakeSize(400, 400)];
161
162  delegate_.reset([[TestAppsSearchResultsDelegate alloc] init]);
163
164  // Populate with some results so that TEST_VIEW does something non-trivial.
165  for (int i = 0; i < kDefaultResultsCount; ++i)
166    AddTestResultAtIndex(i, base::StringPrintf("Result %d", i), "ItemDetail");
167
168  SearchResult::Tags test_tags;
169  // Apply markup to the substring "Result" in the first item.
170  test_tags.push_back(SearchResult::Tag(SearchResult::Tag::NONE, 0, 1));
171  test_tags.push_back(SearchResult::Tag(SearchResult::Tag::URL, 1, 2));
172  test_tags.push_back(SearchResult::Tag(SearchResult::Tag::MATCH, 2, 3));
173  test_tags.push_back(SearchResult::Tag(SearchResult::Tag::DIM, 3, 4));
174  test_tags.push_back(SearchResult::Tag(SearchResult::Tag::MATCH |
175                                        SearchResult::Tag::URL, 4, 5));
176  test_tags.push_back(SearchResult::Tag(SearchResult::Tag::MATCH |
177                                        SearchResult::Tag::DIM, 5, 6));
178
179  SearchResult* result = ModelResultAt(0);
180  result->SetIcon(gfx::ImageSkiaFromNSImage(
181      [NSImage imageNamed:NSImageNameStatusAvailable]));
182  result->set_title_tags(test_tags);
183
184  [apps_search_results_controller_ setDelegate:delegate_];
185
186  ui::CocoaTest::SetUp();
187  [[test_window() contentView] addSubview:
188      [apps_search_results_controller_ view]];
189}
190
191void AppsSearchResultsControllerTest::TearDown() {
192  [apps_search_results_controller_ setDelegate:nil];
193  ui::CocoaTest::TearDown();
194}
195
196NSEvent* MouseEventInRow(NSTableView* table_view, NSInteger row_index) {
197  NSRect row_rect = [table_view rectOfRow:row_index];
198  NSPoint point_in_view = NSMakePoint(NSMidX(row_rect), NSMidY(row_rect));
199  NSPoint point_in_window = [table_view convertPoint:point_in_view
200                                              toView:nil];
201  return cocoa_test_event_utils::LeftMouseDownAtPoint(point_in_window);
202}
203
204}  // namespace
205
206TEST_VIEW(AppsSearchResultsControllerTest,
207          [apps_search_results_controller_ view]);
208
209TEST_F(AppsSearchResultsControllerTest, ModelObservers) {
210  NSTableView* table_view = [apps_search_results_controller_ tableView];
211  ExpectConsistent();
212
213  EXPECT_EQ(1, [table_view numberOfColumns]);
214  EXPECT_EQ(kDefaultResultsCount, [table_view numberOfRows]);
215
216  // Insert at start.
217  AddTestResultAtIndex(0, "One", std::string());
218  EXPECT_EQ(kDefaultResultsCount + 1, [table_view numberOfRows]);
219  ExpectConsistent();
220
221  // Remove from end.
222  [delegate_ appListModel]->results()->DeleteAt(kDefaultResultsCount);
223  EXPECT_EQ(kDefaultResultsCount, [table_view numberOfRows]);
224  ExpectConsistent();
225
226  // Insert at end.
227  AddTestResultAtIndex(kDefaultResultsCount, "Four", std::string());
228  EXPECT_EQ(kDefaultResultsCount + 1, [table_view numberOfRows]);
229  ExpectConsistent();
230
231  // Delete from start.
232  [delegate_ appListModel]->results()->DeleteAt(0);
233  EXPECT_EQ(kDefaultResultsCount, [table_view numberOfRows]);
234  ExpectConsistent();
235
236  // Test clearing results.
237  [delegate_ appListModel]->results()->DeleteAll();
238  EXPECT_EQ(0, [table_view numberOfRows]);
239  ExpectConsistent();
240}
241
242TEST_F(AppsSearchResultsControllerTest, KeyboardSelectAndActivate) {
243  NSTableView* table_view = [apps_search_results_controller_ tableView];
244  EXPECT_EQ(-1, [table_view selectedRow]);
245
246  // Pressing up when nothing is selected should select the last item.
247  EXPECT_TRUE(SimulateKeyAction(@selector(moveUp:)));
248  EXPECT_EQ(kDefaultResultsCount - 1, [table_view selectedRow]);
249  [table_view deselectAll:nil];
250  EXPECT_EQ(-1, [table_view selectedRow]);
251
252  // Pressing down when nothing is selected should select the first item.
253  EXPECT_TRUE(SimulateKeyAction(@selector(moveDown:)));
254  EXPECT_EQ(0, [table_view selectedRow]);
255
256  // Pressing up should wrap around.
257  EXPECT_TRUE(SimulateKeyAction(@selector(moveUp:)));
258  EXPECT_EQ(kDefaultResultsCount - 1, [table_view selectedRow]);
259
260  // Down should now also wrap, since the selection is at the end.
261  EXPECT_TRUE(SimulateKeyAction(@selector(moveDown:)));
262  EXPECT_EQ(0, [table_view selectedRow]);
263
264  // Regular down and up movement, ensuring the cells have correct backgrounds.
265  EXPECT_TRUE(SimulateKeyAction(@selector(moveDown:)));
266  EXPECT_EQ(1, [table_view selectedRow]);
267  EXPECT_EQ(NSBackgroundStyleDark, [ViewResultAt(1) backgroundStyle]);
268  EXPECT_EQ(NSBackgroundStyleLight, [ViewResultAt(0) backgroundStyle]);
269
270  EXPECT_TRUE(SimulateKeyAction(@selector(moveUp:)));
271  EXPECT_EQ(0, [table_view selectedRow]);
272  EXPECT_EQ(NSBackgroundStyleDark, [ViewResultAt(0) backgroundStyle]);
273  EXPECT_EQ(NSBackgroundStyleLight, [ViewResultAt(1) backgroundStyle]);
274
275  // Test activating items.
276  EXPECT_TRUE(SimulateKeyAction(@selector(insertNewline:)));
277  EXPECT_EQ(ModelResultAt(0), [delegate_ lastOpenedResult]);
278  EXPECT_TRUE(SimulateKeyAction(@selector(moveDown:)));
279  EXPECT_TRUE(SimulateKeyAction(@selector(insertNewline:)));
280  EXPECT_EQ(ModelResultAt(1), [delegate_ lastOpenedResult]);
281}
282
283TEST_F(AppsSearchResultsControllerTest, ContextMenus) {
284  NSTableView* table_view = [apps_search_results_controller_ tableView];
285  NSEvent* mouse_in_row_0 = MouseEventInRow(table_view, 0);
286  NSEvent* mouse_in_row_1 = MouseEventInRow(table_view, 1);
287
288  NSMenu* menu = [table_view menuForEvent:mouse_in_row_0];
289  EXPECT_EQ(1, [menu numberOfItems]);
290  EXPECT_NSEQ(@"Menu For: Result 0", [[menu itemAtIndex:0] title]);
291
292  // Test a context menu request while the item is still installing.
293  SetMenuReadyAt(1, false);
294  menu = [table_view menuForEvent:mouse_in_row_1];
295  EXPECT_EQ(nil, menu);
296
297  SetMenuReadyAt(1, true);
298  menu = [table_view menuForEvent:mouse_in_row_1];
299  EXPECT_EQ(1, [menu numberOfItems]);
300  EXPECT_NSEQ(@"Menu For: Result 1", [[menu itemAtIndex:0] title]);
301}
302
303// Test that observing a search result item uninstall performs the search again.
304TEST_F(AppsSearchResultsControllerTest, UninstallReperformsSearch) {
305  base::MessageLoopForUI message_loop;
306  EXPECT_EQ(0, [delegate_ redoSearchCount]);
307  ModelResultAt(0)->NotifyItemUninstalled();
308  [delegate_ performSelector:@selector(quitMessageLoop)
309                  withObject:nil
310                  afterDelay:0];
311  message_loop.Run();
312  EXPECT_EQ(1, [delegate_ redoSearchCount]);
313}
314
315}  // namespace test
316}  // namespace app_list
317