| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ui/app_list/cocoa/apps_search_results_controller.h" |
| |
| #include "base/mac/scoped_nsobject.h" |
| #include "base/message_loop/message_loop.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #import "testing/gtest_mac.h" |
| #include "ui/app_list/test/app_list_test_model.h" |
| #include "ui/app_list/test/test_search_result.h" |
| #include "ui/base/models/simple_menu_model.h" |
| #import "ui/events/test/cocoa_test_event_utils.h" |
| #include "ui/gfx/image/image_skia_util_mac.h" |
| #import "ui/gfx/test/ui_cocoa_test_helper.h" |
| |
| @interface TestAppsSearchResultsDelegate : NSObject<AppsSearchResultsDelegate> { |
| @private |
| app_list::test::AppListTestModel appListModel_; |
| app_list::SearchResult* lastOpenedResult_; |
| int redoSearchCount_; |
| } |
| |
| @property(readonly, nonatomic) app_list::SearchResult* lastOpenedResult; |
| @property(readonly, nonatomic) int redoSearchCount; |
| |
| - (void)quitMessageLoop; |
| |
| @end |
| |
| @implementation TestAppsSearchResultsDelegate |
| |
| @synthesize lastOpenedResult = lastOpenedResult_; |
| @synthesize redoSearchCount = redoSearchCount_; |
| |
| - (app_list::AppListModel*)appListModel { |
| return &appListModel_; |
| } |
| |
| - (void)openResult:(app_list::SearchResult*)result { |
| lastOpenedResult_ = result; |
| } |
| |
| - (void)redoSearch { |
| ++redoSearchCount_; |
| } |
| |
| - (void)quitMessageLoop { |
| base::MessageLoop::current()->QuitNow(); |
| } |
| |
| @end |
| |
| namespace app_list { |
| namespace test { |
| namespace { |
| |
| const int kDefaultResultsCount = 3; |
| |
| class SearchResultWithMenu : public TestSearchResult { |
| public: |
| SearchResultWithMenu(const std::string& title, const std::string& details) |
| : menu_model_(NULL), |
| menu_ready_(true) { |
| set_title(base::ASCIIToUTF16(title)); |
| set_details(base::ASCIIToUTF16(details)); |
| menu_model_.AddItem(0, base::UTF8ToUTF16("Menu For: " + title)); |
| } |
| |
| void SetMenuReadyForTesting(bool ready) { |
| menu_ready_ = ready; |
| } |
| |
| ui::MenuModel* GetContextMenuModel() override { |
| if (!menu_ready_) |
| return NULL; |
| |
| return &menu_model_; |
| } |
| |
| private: |
| ui::SimpleMenuModel menu_model_; |
| bool menu_ready_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SearchResultWithMenu); |
| }; |
| |
| class AppsSearchResultsControllerTest : public ui::CocoaTest { |
| public: |
| AppsSearchResultsControllerTest() {} |
| |
| void AddTestResultAtIndex(size_t index, |
| const std::string& title, |
| const std::string& details) { |
| scoped_ptr<SearchResult> result(new SearchResultWithMenu(title, details)); |
| AppListModel::SearchResults* results = [delegate_ appListModel]->results(); |
| results->AddAt(index, result.release()); |
| } |
| |
| SearchResult* ModelResultAt(size_t index) { |
| return [delegate_ appListModel]->results()->GetItemAt(index); |
| } |
| |
| NSCell* ViewResultAt(NSInteger index) { |
| NSTableView* table_view = [apps_search_results_controller_ tableView]; |
| return [table_view preparedCellAtColumn:0 |
| row:index]; |
| } |
| |
| void SetMenuReadyAt(size_t index, bool ready) { |
| SearchResultWithMenu* result = |
| static_cast<SearchResultWithMenu*>(ModelResultAt(index)); |
| result->SetMenuReadyForTesting(ready); |
| } |
| |
| BOOL SimulateKeyAction(SEL c) { |
| return [apps_search_results_controller_ handleCommandBySelector:c]; |
| } |
| |
| void ExpectConsistent(); |
| |
| // ui::CocoaTest overrides: |
| virtual void SetUp() override; |
| virtual void TearDown() override; |
| |
| protected: |
| base::scoped_nsobject<TestAppsSearchResultsDelegate> delegate_; |
| base::scoped_nsobject<AppsSearchResultsController> |
| apps_search_results_controller_; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(AppsSearchResultsControllerTest); |
| }; |
| |
| void AppsSearchResultsControllerTest::ExpectConsistent() { |
| NSInteger item_count = [delegate_ appListModel]->results()->item_count(); |
| ASSERT_EQ(item_count, |
| [[apps_search_results_controller_ tableView] numberOfRows]); |
| |
| // Compare content strings to ensure the order of items is consistent, and any |
| // model data that should have been reloaded has been reloaded in the view. |
| for (NSInteger i = 0; i < item_count; ++i) { |
| SearchResult* result = ModelResultAt(i); |
| base::string16 string_in_model = result->title(); |
| if (!result->details().empty()) |
| string_in_model += base::ASCIIToUTF16("\n") + result->details(); |
| EXPECT_NSEQ(base::SysUTF16ToNSString(string_in_model), |
| [[ViewResultAt(i) attributedStringValue] string]); |
| } |
| } |
| |
| void AppsSearchResultsControllerTest::SetUp() { |
| apps_search_results_controller_.reset( |
| [[AppsSearchResultsController alloc] initWithAppsSearchResultsFrameSize: |
| NSMakeSize(400, 400)]); |
| // The view is initially hidden. Give it a non-zero height so it draws. |
| [[apps_search_results_controller_ view] setFrameSize:NSMakeSize(400, 400)]; |
| |
| delegate_.reset([[TestAppsSearchResultsDelegate alloc] init]); |
| |
| // Populate with some results so that TEST_VIEW does something non-trivial. |
| for (int i = 0; i < kDefaultResultsCount; ++i) |
| AddTestResultAtIndex(i, base::StringPrintf("Result %d", i), "ItemDetail"); |
| |
| SearchResult::Tags test_tags; |
| // Apply markup to the substring "Result" in the first item. |
| test_tags.push_back(SearchResult::Tag(SearchResult::Tag::NONE, 0, 1)); |
| test_tags.push_back(SearchResult::Tag(SearchResult::Tag::URL, 1, 2)); |
| test_tags.push_back(SearchResult::Tag(SearchResult::Tag::MATCH, 2, 3)); |
| test_tags.push_back(SearchResult::Tag(SearchResult::Tag::DIM, 3, 4)); |
| test_tags.push_back(SearchResult::Tag(SearchResult::Tag::MATCH | |
| SearchResult::Tag::URL, 4, 5)); |
| test_tags.push_back(SearchResult::Tag(SearchResult::Tag::MATCH | |
| SearchResult::Tag::DIM, 5, 6)); |
| |
| SearchResult* result = ModelResultAt(0); |
| result->SetIcon(gfx::ImageSkiaFromNSImage( |
| [NSImage imageNamed:NSImageNameStatusAvailable])); |
| result->set_title_tags(test_tags); |
| |
| [apps_search_results_controller_ setDelegate:delegate_]; |
| |
| ui::CocoaTest::SetUp(); |
| [[test_window() contentView] addSubview: |
| [apps_search_results_controller_ view]]; |
| } |
| |
| void AppsSearchResultsControllerTest::TearDown() { |
| [apps_search_results_controller_ setDelegate:nil]; |
| ui::CocoaTest::TearDown(); |
| } |
| |
| NSEvent* MouseEventInRow(NSTableView* table_view, NSInteger row_index) { |
| NSRect row_rect = [table_view rectOfRow:row_index]; |
| NSPoint point_in_view = NSMakePoint(NSMidX(row_rect), NSMidY(row_rect)); |
| NSPoint point_in_window = [table_view convertPoint:point_in_view |
| toView:nil]; |
| return cocoa_test_event_utils::LeftMouseDownAtPoint(point_in_window); |
| } |
| |
| } // namespace |
| |
| TEST_VIEW(AppsSearchResultsControllerTest, |
| [apps_search_results_controller_ view]); |
| |
| TEST_F(AppsSearchResultsControllerTest, ModelObservers) { |
| NSTableView* table_view = [apps_search_results_controller_ tableView]; |
| ExpectConsistent(); |
| |
| EXPECT_EQ(1, [table_view numberOfColumns]); |
| EXPECT_EQ(kDefaultResultsCount, [table_view numberOfRows]); |
| |
| // Insert at start. |
| AddTestResultAtIndex(0, "One", std::string()); |
| EXPECT_EQ(kDefaultResultsCount + 1, [table_view numberOfRows]); |
| ExpectConsistent(); |
| |
| // Remove from end. |
| [delegate_ appListModel]->results()->DeleteAt(kDefaultResultsCount); |
| EXPECT_EQ(kDefaultResultsCount, [table_view numberOfRows]); |
| ExpectConsistent(); |
| |
| // Insert at end. |
| AddTestResultAtIndex(kDefaultResultsCount, "Four", std::string()); |
| EXPECT_EQ(kDefaultResultsCount + 1, [table_view numberOfRows]); |
| ExpectConsistent(); |
| |
| // Delete from start. |
| [delegate_ appListModel]->results()->DeleteAt(0); |
| EXPECT_EQ(kDefaultResultsCount, [table_view numberOfRows]); |
| ExpectConsistent(); |
| |
| // Test clearing results. |
| [delegate_ appListModel]->results()->DeleteAll(); |
| EXPECT_EQ(0, [table_view numberOfRows]); |
| ExpectConsistent(); |
| } |
| |
| TEST_F(AppsSearchResultsControllerTest, KeyboardSelectAndActivate) { |
| NSTableView* table_view = [apps_search_results_controller_ tableView]; |
| EXPECT_EQ(-1, [table_view selectedRow]); |
| |
| // Pressing up when nothing is selected should select the last item. |
| EXPECT_TRUE(SimulateKeyAction(@selector(moveUp:))); |
| EXPECT_EQ(kDefaultResultsCount - 1, [table_view selectedRow]); |
| [table_view deselectAll:nil]; |
| EXPECT_EQ(-1, [table_view selectedRow]); |
| |
| // Pressing down when nothing is selected should select the first item. |
| EXPECT_TRUE(SimulateKeyAction(@selector(moveDown:))); |
| EXPECT_EQ(0, [table_view selectedRow]); |
| |
| // Pressing up should wrap around. |
| EXPECT_TRUE(SimulateKeyAction(@selector(moveUp:))); |
| EXPECT_EQ(kDefaultResultsCount - 1, [table_view selectedRow]); |
| |
| // Down should now also wrap, since the selection is at the end. |
| EXPECT_TRUE(SimulateKeyAction(@selector(moveDown:))); |
| EXPECT_EQ(0, [table_view selectedRow]); |
| |
| // Regular down and up movement, ensuring the cells have correct backgrounds. |
| EXPECT_TRUE(SimulateKeyAction(@selector(moveDown:))); |
| EXPECT_EQ(1, [table_view selectedRow]); |
| EXPECT_EQ(NSBackgroundStyleDark, [ViewResultAt(1) backgroundStyle]); |
| EXPECT_EQ(NSBackgroundStyleLight, [ViewResultAt(0) backgroundStyle]); |
| |
| EXPECT_TRUE(SimulateKeyAction(@selector(moveUp:))); |
| EXPECT_EQ(0, [table_view selectedRow]); |
| EXPECT_EQ(NSBackgroundStyleDark, [ViewResultAt(0) backgroundStyle]); |
| EXPECT_EQ(NSBackgroundStyleLight, [ViewResultAt(1) backgroundStyle]); |
| |
| // Test activating items. |
| EXPECT_TRUE(SimulateKeyAction(@selector(insertNewline:))); |
| EXPECT_EQ(ModelResultAt(0), [delegate_ lastOpenedResult]); |
| EXPECT_TRUE(SimulateKeyAction(@selector(moveDown:))); |
| EXPECT_TRUE(SimulateKeyAction(@selector(insertNewline:))); |
| EXPECT_EQ(ModelResultAt(1), [delegate_ lastOpenedResult]); |
| } |
| |
| TEST_F(AppsSearchResultsControllerTest, ContextMenus) { |
| NSTableView* table_view = [apps_search_results_controller_ tableView]; |
| NSEvent* mouse_in_row_0 = MouseEventInRow(table_view, 0); |
| NSEvent* mouse_in_row_1 = MouseEventInRow(table_view, 1); |
| |
| NSMenu* menu = [table_view menuForEvent:mouse_in_row_0]; |
| EXPECT_EQ(1, [menu numberOfItems]); |
| EXPECT_NSEQ(@"Menu For: Result 0", [[menu itemAtIndex:0] title]); |
| |
| // Test a context menu request while the item is still installing. |
| SetMenuReadyAt(1, false); |
| menu = [table_view menuForEvent:mouse_in_row_1]; |
| EXPECT_EQ(nil, menu); |
| |
| SetMenuReadyAt(1, true); |
| menu = [table_view menuForEvent:mouse_in_row_1]; |
| EXPECT_EQ(1, [menu numberOfItems]); |
| EXPECT_NSEQ(@"Menu For: Result 1", [[menu itemAtIndex:0] title]); |
| } |
| |
| // Test that observing a search result item uninstall performs the search again. |
| TEST_F(AppsSearchResultsControllerTest, UninstallReperformsSearch) { |
| base::MessageLoopForUI message_loop; |
| EXPECT_EQ(0, [delegate_ redoSearchCount]); |
| ModelResultAt(0)->NotifyItemUninstalled(); |
| [delegate_ performSelector:@selector(quitMessageLoop) |
| withObject:nil |
| afterDelay:0]; |
| message_loop.Run(); |
| EXPECT_EQ(1, [delegate_ redoSearchCount]); |
| } |
| |
| } // namespace test |
| } // namespace app_list |