| // Copyright (c) 2012 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 <UIKit/UIKit.h> |
| |
| #include <memory> |
| |
| #include "base/memory/ptr_util.h" |
| #import "base/test/ios/wait_util.h" |
| #include "base/time/time.h" |
| #include "components/toolbar/test_toolbar_model.h" |
| #include "ios/chrome/browser/autocomplete/autocomplete_classifier_factory.h" |
| #include "ios/chrome/browser/browser_state/test_chrome_browser_state.h" |
| #include "ios/chrome/browser/search_engines/template_url_service_factory.h" |
| #import "ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.h" |
| #include "ios/chrome/browser/ui/toolbar/toolbar_model_delegate_ios.h" |
| #include "ios/chrome/browser/ui/toolbar/toolbar_model_impl_ios.h" |
| #import "ios/chrome/browser/ui/toolbar/web_toolbar_controller.h" |
| #include "ios/chrome/test/base/perf_test_ios.h" |
| #include "ios/shared/chrome/browser/tabs/fake_web_state_list_delegate.h" |
| #include "ios/shared/chrome/browser/tabs/web_state_list.h" |
| #include "ios/web/public/test/fakes/test_web_state.h" |
| #include "testing/platform_test.h" |
| #import "third_party/ocmock/OCMock/OCMock.h" |
| #import "ui/base/test/ios/keyboard_appearance_listener.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| |
| // Descends down a view hierarchy until the first view of |specificClass| |
| // is found. Returns nil if a view of |specificClass| cannot be found. |
| UIView* FindViewByClass(UIView* topView, Class specificClass) { |
| if ([topView isKindOfClass:specificClass]) |
| return [topView window] ? topView : nil; |
| for (UIView* subview in [topView subviews]) { |
| UIView* foundView = FindViewByClass(subview, specificClass); |
| if (foundView) |
| return foundView; |
| } |
| return nil; |
| } |
| |
| // Constant for UI wait loop. |
| const NSTimeInterval kSpinDelaySeconds = 0.01; |
| |
| class OmniboxPerfTest : public PerfTest { |
| public: |
| OmniboxPerfTest() : PerfTest("Omnibox") {} |
| |
| protected: |
| void SetUp() override { |
| PerfTest::SetUp(); |
| TestChromeBrowserState::Builder test_cbs_builder; |
| test_cbs_builder.AddTestingFactory( |
| ios::TemplateURLServiceFactory::GetInstance(), |
| ios::TemplateURLServiceFactory::GetDefaultFactory()); |
| test_cbs_builder.AddTestingFactory( |
| ios::AutocompleteClassifierFactory::GetInstance(), |
| ios::AutocompleteClassifierFactory::GetDefaultFactory()); |
| chrome_browser_state_ = test_cbs_builder.Build(); |
| |
| // Prepares testing profile for autocompletion. |
| ios::AutocompleteClassifierFactory::GetForBrowserState( |
| chrome_browser_state_.get()); |
| |
| // Sets up the listener for keyboard activation/deactivation notifications. |
| keyboard_listener_ = [[KeyboardAppearanceListener alloc] init]; |
| |
| // Create a real window to host the Toolbar. |
| CGRect screenBounds = [[UIScreen mainScreen] bounds]; |
| window_ = [[UIWindow alloc] initWithFrame:screenBounds]; |
| [window_ makeKeyAndVisible]; |
| |
| // Create a WebStateList that will always return the test WebState as |
| // the active WebState. |
| web_state_list_ = base::MakeUnique<WebStateList>(&web_state_list_delegate_); |
| std::unique_ptr<web::TestWebState> web_state = |
| base::MakeUnique<web::TestWebState>(); |
| web_state_list_->InsertWebState(0, std::move(web_state)); |
| |
| // Creates the Toolbar for testing and sizes it to the width of the screen. |
| toolbar_model_delegate_.reset( |
| new ToolbarModelDelegateIOS(web_state_list_.get())); |
| toolbar_model_ios_.reset( |
| new ToolbarModelImplIOS(toolbar_model_delegate_.get())); |
| |
| // The OCMOCK_VALUE macro doesn't like std::unique_ptr, but it works just |
| // fine if a temporary variable is used. |
| ToolbarModelIOS* model_for_mock = toolbar_model_ios_.get(); |
| id webToolbarDelegate = |
| [OCMockObject niceMockForProtocol:@protocol(WebToolbarDelegate)]; |
| [[[webToolbarDelegate stub] andReturnValue:OCMOCK_VALUE(model_for_mock)] |
| toolbarModelIOS]; |
| id urlLoader = [OCMockObject niceMockForProtocol:@protocol(UrlLoader)]; |
| toolbar_ = [[WebToolbarController alloc] |
| initWithDelegate:webToolbarDelegate |
| urlLoader:urlLoader |
| browserState:chrome_browser_state_.get() |
| preloadProvider:nil]; |
| UIView* toolbarView = [toolbar_ view]; |
| CGRect toolbarFrame = toolbarView.frame; |
| toolbarFrame.origin = CGPointZero; |
| toolbarFrame.size.width = screenBounds.size.width; |
| toolbarView.frame = toolbarFrame; |
| // Add toolbar to window. |
| [window_ addSubview:toolbarView]; |
| base::test::ios::WaitUntilCondition(^bool() { |
| return IsToolbarLoaded(window_); |
| }); |
| } |
| |
| void TearDown() override { |
| // Remove toolbar from window. |
| [[toolbar_ view] removeFromSuperview]; |
| base::test::ios::WaitUntilCondition(^bool() { |
| return !IsToolbarLoaded(window_); |
| }); |
| [toolbar_ browserStateDestroyed]; |
| PerfTest::TearDown(); |
| } |
| |
| // Returns whether Omnibox has been loaded. |
| bool IsToolbarLoaded(UIView* topView) { |
| return FindViewByClass(topView, [OmniboxTextFieldIOS class]) != nil; |
| } |
| |
| // Inserts the |text| string into the |textField| and return the amount |
| // of time it took to complete the insertion. This does not time |
| // any activities that may be triggered on other threads. |
| base::TimeDelta TimeInsertText(OmniboxTextFieldIOS* textField, |
| NSString* text) { |
| base::Time startTime = base::Time::NowFromSystemTime(); |
| [textField insertTextWhileEditing:text]; |
| base::TimeDelta elapsed = base::Time::NowFromSystemTime() - startTime; |
| // Adds a small delay so the run loop can update the screen. |
| // The user experience measurement should include this visual |
| // feedback to the user, but there is no way of catching when |
| // the typed character showed up in the omnibox on screen. |
| base::test::ios::SpinRunLoopWithMaxDelay( |
| base::TimeDelta::FromSecondsD(kSpinDelaySeconds)); |
| return elapsed; |
| } |
| |
| // Creates a dummy text field and make it be a first responder so keyboard |
| // comes up. In general, there's a lot of time spent in loading up various |
| // graphics assets on a keyboard which may distort the measurement of Chrome |
| // Omnibox focus timings. Call this function to preload keyboard before |
| // doing the real test. |
| base::TimeDelta PreLoadKeyboard() { |
| UITextField* textField = |
| [[UITextField alloc] initWithFrame:CGRectMake(20, 100, 280, 29)]; |
| [textField setBorderStyle:UITextBorderStyleRoundedRect]; |
| [window_ addSubview:textField]; |
| base::TimeDelta elapsed = base::test::ios::TimeUntilCondition( |
| ^{ |
| [textField becomeFirstResponder]; |
| }, |
| ^bool() { |
| return [keyboard_listener_ isKeyboardVisible]; |
| }, |
| false, base::TimeDelta()); |
| base::test::ios::TimeUntilCondition( |
| ^{ |
| [textField resignFirstResponder]; |
| }, |
| ^bool() { |
| return ![keyboard_listener_ isKeyboardVisible]; |
| }, |
| false, base::TimeDelta()); |
| [textField removeFromSuperview]; |
| return elapsed; |
| } |
| |
| // Enables the on-screen keyboard as if user has tapped on |textField|. |
| // Returns the amount of time it took for the keyboard to appear. |
| base::TimeDelta EnableKeyboard(OmniboxTextFieldIOS* textField) { |
| return base::test::ios::TimeUntilCondition( |
| ^{ |
| [textField becomeFirstResponder]; |
| }, |
| ^bool() { |
| return [keyboard_listener_ isKeyboardVisible]; |
| }, |
| false, base::TimeDelta()); |
| } |
| |
| // Performs necessary cleanup (so next pass of unit test can start from |
| // a clean slate) and then exit from |textField| to dismiss keyboard. |
| void DisableKeyboard(OmniboxTextFieldIOS* textField) { |
| // Busy wait until keyboard is hidden. |
| base::test::ios::TimeUntilCondition( |
| ^{ |
| [textField exitPreEditState]; |
| [textField resignFirstResponder]; |
| }, |
| ^bool() { |
| return ![keyboard_listener_ isKeyboardVisible]; |
| }, |
| false, base::TimeDelta()); |
| } |
| |
| std::unique_ptr<TestChromeBrowserState> chrome_browser_state_; |
| FakeWebStateListDelegate web_state_list_delegate_; |
| std::unique_ptr<WebStateList> web_state_list_; |
| std::unique_ptr<ToolbarModelDelegateIOS> toolbar_model_delegate_; |
| std::unique_ptr<ToolbarModelIOS> toolbar_model_ios_; |
| WebToolbarController* toolbar_; |
| UIWindow* window_; |
| KeyboardAppearanceListener* keyboard_listener_; |
| }; |
| |
| // Measures the amount of time it takes the Omnibox text field to activate |
| // the on-screen keyboard. |
| TEST_F(OmniboxPerfTest, TestTextFieldDidBeginEditing) { |
| LogPerfTiming("Keyboard preload", PreLoadKeyboard()); |
| OmniboxTextFieldIOS* textField = (OmniboxTextFieldIOS*)FindViewByClass( |
| [toolbar_ view], [OmniboxTextFieldIOS class]); |
| |
| // Time how long it takes to "focus" on omnibox. |
| RepeatTimedRuns("Begin editing", |
| ^base::TimeDelta(int index) { |
| return EnableKeyboard(textField); |
| }, |
| ^() { |
| DisableKeyboard(textField); |
| }); |
| } |
| |
| // Measures the amount of time it takes to type in the first character |
| // into the Omnibox. |
| TEST_F(OmniboxPerfTest, TestTypeOneCharInTextField) { |
| OmniboxTextFieldIOS* textField = (OmniboxTextFieldIOS*)FindViewByClass( |
| [toolbar_ view], [OmniboxTextFieldIOS class]); |
| RepeatTimedRuns("Type first character", |
| ^base::TimeDelta(int index) { |
| EnableKeyboard(textField); |
| return TimeInsertText(textField, @"G"); |
| }, |
| ^() { |
| [textField setText:@""]; |
| DisableKeyboard(textField); |
| }); |
| } |
| |
| // Measures the amount of time it takes to type in the word "google" one |
| // letter at a time. |
| TEST_F(OmniboxPerfTest, TestTypingInTextField) { |
| OmniboxTextFieldIOS* textField = (OmniboxTextFieldIOS*)FindViewByClass( |
| [toolbar_ view], [OmniboxTextFieldIOS class]); |
| // The characters to type into the omnibox text field. |
| NSArray* inputCharacters = |
| [NSArray arrayWithObjects:@"g", @"o", @"o", @"g", @"l", @"e", nil]; |
| RepeatTimedRuns( |
| "Typing", |
| ^base::TimeDelta(int index) { |
| EnableKeyboard(textField); |
| NSMutableString* logMessage = [NSMutableString string]; |
| base::TimeDelta elapsed; |
| for (NSString* input in inputCharacters) { |
| base::TimeDelta inputElapsed = TimeInsertText(textField, input); |
| [logMessage appendFormat:@"%@'%@':%.0f", |
| [logMessage length] ? @" " : @"", input, |
| inputElapsed.InMillisecondsF()]; |
| elapsed += inputElapsed; |
| } |
| NSLog(@"%2d: %@", index, logMessage); |
| return elapsed; |
| }, |
| ^() { |
| [textField setText:@""]; |
| DisableKeyboard(textField); |
| }); |
| } |
| } |