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#import "chrome/browser/ui/cocoa/browser_window_controller.h"
6
7#import "base/mac/mac_util.h"
8#include "base/mac/sdk_forward_declarations.h"
9#include "base/run_loop.h"
10#include "base/strings/utf_string_conversions.h"
11#include "chrome/app/chrome_command_ids.h"
12#include "chrome/browser/browser_process.h"
13#include "chrome/browser/devtools/devtools_window_testing.h"
14#include "chrome/browser/infobars/infobar_service.h"
15#include "chrome/browser/infobars/simple_alert_infobar_delegate.h"
16#include "chrome/browser/profiles/profile.h"
17#include "chrome/browser/profiles/profile_manager.h"
18#include "chrome/browser/ui/bookmarks/bookmark_utils.h"
19#include "chrome/browser/ui/browser.h"
20#include "chrome/browser/ui/browser_commands.h"
21#include "chrome/browser/ui/browser_list.h"
22#include "chrome/browser/ui/browser_window.h"
23#include "chrome/browser/ui/cocoa/browser_window_cocoa.h"
24#import "chrome/browser/ui/cocoa/browser_window_controller_private.h"
25#import "chrome/browser/ui/cocoa/fast_resize_view.h"
26#import "chrome/browser/ui/cocoa/history_overlay_controller.h"
27#import "chrome/browser/ui/cocoa/infobars/infobar_cocoa.h"
28#import "chrome/browser/ui/cocoa/infobars/infobar_container_controller.h"
29#import "chrome/browser/ui/cocoa/infobars/infobar_controller.h"
30#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
31#import "chrome/browser/ui/cocoa/profiles/avatar_base_controller.h"
32#import "chrome/browser/ui/cocoa/tab_contents/overlayable_contents_controller.h"
33#import "chrome/browser/ui/cocoa/tabs/tab_strip_view.h"
34#include "chrome/browser/ui/extensions/application_launch.h"
35#include "chrome/browser/ui/find_bar/find_bar.h"
36#include "chrome/browser/ui/find_bar/find_bar_controller.h"
37#include "chrome/browser/ui/tabs/tab_strip_model.h"
38#include "chrome/test/base/in_process_browser_test.h"
39#include "chrome/test/base/testing_profile.h"
40#include "content/public/browser/web_contents.h"
41#include "content/public/test/test_utils.h"
42#import "testing/gtest_mac.h"
43#import "third_party/ocmock/OCMock/OCMock.h"
44#import "ui/base/cocoa/nsview_additions.h"
45#include "ui/gfx/animation/slide_animation.h"
46
47namespace {
48
49// Creates a mock of an NSWindow that has the given |frame|.
50id MockWindowWithFrame(NSRect frame) {
51  id window = [OCMockObject mockForClass:[NSWindow class]];
52  NSValue* window_frame =
53      [NSValue valueWithBytes:&frame objCType:@encode(NSRect)];
54  [[[window stub] andReturnValue:window_frame] frame];
55  return window;
56}
57
58void CreateProfileCallback(const base::Closure& quit_closure,
59                           Profile* profile,
60                           Profile::CreateStatus status) {
61  EXPECT_TRUE(profile);
62  EXPECT_NE(Profile::CREATE_STATUS_LOCAL_FAIL, status);
63  EXPECT_NE(Profile::CREATE_STATUS_REMOTE_FAIL, status);
64  // This will be called multiple times. Wait until the profile is initialized
65  // fully to quit the loop.
66  if (status == Profile::CREATE_STATUS_INITIALIZED)
67    quit_closure.Run();
68}
69
70enum ViewID {
71  VIEW_ID_TOOLBAR,
72  VIEW_ID_BOOKMARK_BAR,
73  VIEW_ID_INFO_BAR,
74  VIEW_ID_FIND_BAR,
75  VIEW_ID_DOWNLOAD_SHELF,
76  VIEW_ID_TAB_CONTENT_AREA,
77  VIEW_ID_FULLSCREEN_FLOATING_BAR,
78  VIEW_ID_COUNT,
79};
80
81}  // namespace
82
83@interface InfoBarContainerController(TestingAPI)
84- (BOOL)isTopInfoBarAnimationRunning;
85@end
86
87@implementation InfoBarContainerController(TestingAPI)
88- (BOOL)isTopInfoBarAnimationRunning {
89  InfoBarController* infoBarController = [infobarControllers_ objectAtIndex:0];
90  if (infoBarController) {
91    const gfx::SlideAnimation& infobarAnimation =
92        static_cast<const InfoBarCocoa*>(
93            infoBarController.infobar)->animation();
94    return infobarAnimation.is_animating();
95  }
96  return NO;
97}
98@end
99
100class BrowserWindowControllerTest : public InProcessBrowserTest {
101 public:
102  BrowserWindowControllerTest() : InProcessBrowserTest() {
103  }
104
105  virtual void SetUpOnMainThread() OVERRIDE {
106    [[controller() bookmarkBarController] setStateAnimationsEnabled:NO];
107    [[controller() bookmarkBarController] setInnerContentAnimationsEnabled:NO];
108  }
109
110  BrowserWindowController* controller() const {
111    return [BrowserWindowController browserWindowControllerForWindow:
112        browser()->window()->GetNativeWindow()];
113  }
114
115  static void ShowInfoBar(Browser* browser) {
116    SimpleAlertInfoBarDelegate::Create(
117        InfoBarService::FromWebContents(
118            browser->tab_strip_model()->GetActiveWebContents()),
119        0, base::string16(), false);
120  }
121
122  NSView* GetViewWithID(ViewID view_id) const {
123    switch (view_id) {
124      case VIEW_ID_FULLSCREEN_FLOATING_BAR:
125        return [controller() floatingBarBackingView];
126      case VIEW_ID_TOOLBAR:
127        return [[controller() toolbarController] view];
128      case VIEW_ID_BOOKMARK_BAR:
129        return [[controller() bookmarkBarController] view];
130      case VIEW_ID_INFO_BAR:
131        return [[controller() infoBarContainerController] view];
132      case VIEW_ID_FIND_BAR:
133        return [[controller() findBarCocoaController] view];
134      case VIEW_ID_DOWNLOAD_SHELF:
135        return [[controller() downloadShelf] view];
136      case VIEW_ID_TAB_CONTENT_AREA:
137        return [controller() tabContentArea];
138      default:
139        NOTREACHED();
140        return nil;
141    }
142  }
143
144  void VerifyZOrder(const std::vector<ViewID>& view_list) const {
145    std::vector<NSView*> visible_views;
146    for (size_t i = 0; i < view_list.size(); ++i) {
147      NSView* view = GetViewWithID(view_list[i]);
148      if ([view superview])
149        visible_views.push_back(view);
150    }
151
152    for (size_t i = 0; i < visible_views.size() - 1; ++i) {
153      NSView* bottom_view = visible_views[i];
154      NSView* top_view = visible_views[i + 1];
155
156      EXPECT_NSEQ([bottom_view superview], [top_view superview]);
157      EXPECT_TRUE([bottom_view cr_isBelowView:top_view]);
158    }
159
160    // Views not in |view_list| must either be nil or not parented.
161    for (size_t i = 0; i < VIEW_ID_COUNT; ++i) {
162      if (std::find(view_list.begin(), view_list.end(), i) == view_list.end()) {
163        NSView* view = GetViewWithID(static_cast<ViewID>(i));
164        EXPECT_TRUE(!view || ![view superview]);
165      }
166    }
167  }
168
169  CGFloat GetViewHeight(ViewID viewID) const {
170    CGFloat height = NSHeight([GetViewWithID(viewID) frame]);
171    if (viewID == VIEW_ID_INFO_BAR) {
172      height -= [[controller() infoBarContainerController]
173          overlappingTipHeight];
174    }
175    return height;
176  }
177
178  static void CheckTopInfoBarAnimation(
179      InfoBarContainerController* info_bar_container_controller,
180      const base::Closure& quit_task) {
181    if (![info_bar_container_controller isTopInfoBarAnimationRunning])
182      quit_task.Run();
183  }
184
185  static void CheckBookmarkBarAnimation(
186      BookmarkBarController* bookmark_bar_controller,
187      const base::Closure& quit_task) {
188    if (![bookmark_bar_controller isAnimationRunning])
189      quit_task.Run();
190  }
191
192  void WaitForTopInfoBarAnimationToFinish() {
193    scoped_refptr<content::MessageLoopRunner> runner =
194        new content::MessageLoopRunner;
195
196    base::Timer timer(false, true);
197    timer.Start(
198        FROM_HERE,
199        base::TimeDelta::FromMilliseconds(15),
200        base::Bind(&CheckTopInfoBarAnimation,
201                   [controller() infoBarContainerController],
202                   runner->QuitClosure()));
203    runner->Run();
204  }
205
206  void WaitForBookmarkBarAnimationToFinish() {
207    scoped_refptr<content::MessageLoopRunner> runner =
208        new content::MessageLoopRunner;
209
210    base::Timer timer(false, true);
211    timer.Start(
212        FROM_HERE,
213        base::TimeDelta::FromMilliseconds(15),
214        base::Bind(&CheckBookmarkBarAnimation,
215                   [controller() bookmarkBarController],
216                   runner->QuitClosure()));
217    runner->Run();
218  }
219
220  NSInteger GetExpectedTopInfoBarTipHeight() {
221    InfoBarContainerController* info_bar_container_controller =
222        [controller() infoBarContainerController];
223    CGFloat overlapping_tip_height =
224        [info_bar_container_controller overlappingTipHeight];
225    LocationBarViewMac* location_bar_view = [controller() locationBarBridge];
226    NSPoint icon_bottom = location_bar_view->GetPageInfoBubblePoint();
227
228    NSPoint info_bar_top = NSMakePoint(0,
229        NSHeight([info_bar_container_controller view].frame) -
230        overlapping_tip_height);
231    info_bar_top = [[info_bar_container_controller view]
232        convertPoint:info_bar_top toView:nil];
233    return icon_bottom.y - info_bar_top.y;
234  }
235
236  // The traffic lights should always be in front of the content view and the
237  // tab strip view. Since the traffic lights change across OSX versions, this
238  // test verifies that the contentView is in the back, and if the tab strip
239  // view is a sibling, it is directly in front of the content view.
240  void VerifyTrafficLightZOrder() const {
241    NSView* contentView = [[controller() window] contentView];
242    NSView* rootView = [contentView superview];
243    EXPECT_EQ(contentView, [[rootView subviews] objectAtIndex:0]);
244
245    NSView* tabStripView = [controller() tabStripView];
246    if ([[rootView subviews] containsObject:tabStripView])
247      EXPECT_EQ(tabStripView, [[rootView subviews] objectAtIndex:1]);
248  }
249
250 private:
251  DISALLOW_COPY_AND_ASSIGN(BrowserWindowControllerTest);
252};
253
254// Tests that adding the first profile moves the Lion fullscreen button over
255// correctly.
256// DISABLED_ because it regularly times out: http://crbug.com/159002.
257IN_PROC_BROWSER_TEST_F(BrowserWindowControllerTest,
258                       DISABLED_ProfileAvatarFullscreenButton) {
259  if (base::mac::IsOSSnowLeopard())
260    return;
261
262  // Initialize the locals.
263  ProfileManager* profile_manager = g_browser_process->profile_manager();
264  ASSERT_TRUE(profile_manager);
265
266  NSWindow* window = browser()->window()->GetNativeWindow();
267  ASSERT_TRUE(window);
268
269  // With only one profile, the fullscreen button should be visible, but the
270  // avatar button should not.
271  EXPECT_EQ(1u, profile_manager->GetNumberOfProfiles());
272
273  NSButton* fullscreen_button =
274      [window standardWindowButton:NSWindowFullScreenButton];
275  EXPECT_TRUE(fullscreen_button);
276  EXPECT_FALSE([fullscreen_button isHidden]);
277
278  AvatarBaseController* avatar_controller =
279      [controller() avatarButtonController];
280  NSView* avatar = [avatar_controller view];
281  EXPECT_TRUE(avatar);
282  EXPECT_TRUE([avatar isHidden]);
283
284  // Create a profile asynchronously and run the loop until its creation
285  // is complete.
286  base::RunLoop run_loop;
287
288  ProfileManager::CreateCallback create_callback =
289      base::Bind(&CreateProfileCallback, run_loop.QuitClosure());
290  profile_manager->CreateProfileAsync(
291      profile_manager->user_data_dir().Append("test"),
292      create_callback,
293      base::ASCIIToUTF16("avatar_test"),
294      base::string16(),
295      std::string());
296
297  run_loop.Run();
298
299  // There should now be two profiles, and the avatar button and fullscreen
300  // button are both visible.
301  EXPECT_EQ(2u, profile_manager->GetNumberOfProfiles());
302  EXPECT_FALSE([avatar isHidden]);
303  EXPECT_FALSE([fullscreen_button isHidden]);
304  EXPECT_EQ([avatar window], [fullscreen_button window]);
305
306  // Make sure the visual order of the buttons is correct and that they don't
307  // overlap.
308  NSRect avatar_frame = [avatar frame];
309  NSRect fullscreen_frame = [fullscreen_button frame];
310
311  EXPECT_LT(NSMinX(fullscreen_frame), NSMinX(avatar_frame));
312  EXPECT_LT(NSMaxX(fullscreen_frame), NSMinX(avatar_frame));
313}
314
315// Verify that in non-Instant normal mode that the find bar and download shelf
316// are above the content area. Everything else should be below it.
317IN_PROC_BROWSER_TEST_F(BrowserWindowControllerTest, ZOrderNormal) {
318  browser()->GetFindBarController();  // add find bar
319
320  std::vector<ViewID> view_list;
321  view_list.push_back(VIEW_ID_DOWNLOAD_SHELF);
322  view_list.push_back(VIEW_ID_BOOKMARK_BAR);
323  view_list.push_back(VIEW_ID_TOOLBAR);
324  view_list.push_back(VIEW_ID_INFO_BAR);
325  view_list.push_back(VIEW_ID_TAB_CONTENT_AREA);
326  view_list.push_back(VIEW_ID_FIND_BAR);
327  VerifyZOrder(view_list);
328
329  [controller() showOverlay];
330  [controller() removeOverlay];
331  VerifyZOrder(view_list);
332
333  [controller() enterImmersiveFullscreen];
334  [controller() exitImmersiveFullscreen];
335  VerifyZOrder(view_list);
336}
337
338// Verify that in non-Instant presentation mode that the info bar is below the
339// content are and everything else is above it.
340// DISABLED due to flaky failures on trybots. http://crbug.com/178778
341IN_PROC_BROWSER_TEST_F(BrowserWindowControllerTest,
342                       DISABLED_ZOrderPresentationMode) {
343  chrome::ToggleFullscreenMode(browser());
344  browser()->GetFindBarController();  // add find bar
345
346  std::vector<ViewID> view_list;
347  view_list.push_back(VIEW_ID_INFO_BAR);
348  view_list.push_back(VIEW_ID_TAB_CONTENT_AREA);
349  view_list.push_back(VIEW_ID_FULLSCREEN_FLOATING_BAR);
350  view_list.push_back(VIEW_ID_BOOKMARK_BAR);
351  view_list.push_back(VIEW_ID_TOOLBAR);
352  view_list.push_back(VIEW_ID_FIND_BAR);
353  view_list.push_back(VIEW_ID_DOWNLOAD_SHELF);
354  VerifyZOrder(view_list);
355}
356
357// Verify that if the fullscreen floating bar view is below the tab content area
358// then calling |updateSubviewZOrder:| will correctly move back above.
359IN_PROC_BROWSER_TEST_F(BrowserWindowControllerTest,
360                       DISABLED_FloatingBarBelowContentView) {
361  // TODO(kbr): re-enable: http://crbug.com/222296
362  if (base::mac::IsOSMountainLionOrLater())
363    return;
364
365  chrome::ToggleFullscreenMode(browser());
366
367  NSView* fullscreen_floating_bar =
368      GetViewWithID(VIEW_ID_FULLSCREEN_FLOATING_BAR);
369  [fullscreen_floating_bar removeFromSuperview];
370  [[[controller() window] contentView] addSubview:fullscreen_floating_bar
371                                       positioned:NSWindowBelow
372                                       relativeTo:nil];
373  [controller() updateSubviewZOrder];
374
375  std::vector<ViewID> view_list;
376  view_list.push_back(VIEW_ID_INFO_BAR);
377  view_list.push_back(VIEW_ID_TAB_CONTENT_AREA);
378  view_list.push_back(VIEW_ID_FULLSCREEN_FLOATING_BAR);
379  view_list.push_back(VIEW_ID_BOOKMARK_BAR);
380  view_list.push_back(VIEW_ID_TOOLBAR);
381  view_list.push_back(VIEW_ID_DOWNLOAD_SHELF);
382  VerifyZOrder(view_list);
383}
384
385IN_PROC_BROWSER_TEST_F(BrowserWindowControllerTest, SheetPosition) {
386  ASSERT_TRUE([controller() isKindOfClass:[BrowserWindowController class]]);
387  EXPECT_TRUE([controller() isTabbedWindow]);
388  EXPECT_TRUE([controller() hasTabStrip]);
389  EXPECT_FALSE([controller() hasTitleBar]);
390  EXPECT_TRUE([controller() hasToolbar]);
391  EXPECT_FALSE([controller() isBookmarkBarVisible]);
392
393  NSRect defaultAlertFrame = NSMakeRect(0, 0, 300, 200);
394  id sheet = MockWindowWithFrame(defaultAlertFrame);
395  NSWindow* window = browser()->window()->GetNativeWindow();
396  NSRect alertFrame = [controller() window:window
397                         willPositionSheet:nil
398                                 usingRect:defaultAlertFrame];
399  NSRect toolbarFrame = [[[controller() toolbarController] view] frame];
400  EXPECT_EQ(NSMinY(alertFrame), NSMinY(toolbarFrame));
401
402  // Open sheet with normal browser window, persistent bookmark bar.
403  chrome::ToggleBookmarkBarWhenVisible(browser()->profile());
404  EXPECT_TRUE([controller() isBookmarkBarVisible]);
405  alertFrame = [controller() window:window
406                  willPositionSheet:sheet
407                          usingRect:defaultAlertFrame];
408  NSRect bookmarkBarFrame = [[[controller() bookmarkBarController] view] frame];
409  EXPECT_EQ(NSMinY(alertFrame), NSMinY(bookmarkBarFrame));
410
411  // If the sheet is too large, it should be positioned at the top of the
412  // window.
413  defaultAlertFrame = NSMakeRect(0, 0, 300, 2000);
414  sheet = MockWindowWithFrame(defaultAlertFrame);
415  alertFrame = [controller() window:window
416                  willPositionSheet:sheet
417                          usingRect:defaultAlertFrame];
418  EXPECT_EQ(NSMinY(alertFrame), NSHeight([window frame]));
419
420  // Reset the sheet's size.
421  defaultAlertFrame = NSMakeRect(0, 0, 300, 200);
422  sheet = MockWindowWithFrame(defaultAlertFrame);
423
424  // Make sure the profile does not have the bookmark visible so that
425  // we'll create the shortcut window without the bookmark bar.
426  chrome::ToggleBookmarkBarWhenVisible(browser()->profile());
427  // Open application mode window.
428  OpenAppShortcutWindow(browser()->profile(), GURL("about:blank"));
429  Browser* popup_browser = BrowserList::GetInstance(
430      chrome::GetActiveDesktop())->GetLastActive();
431  NSWindow* popupWindow = popup_browser->window()->GetNativeWindow();
432  BrowserWindowController* popupController =
433      [BrowserWindowController browserWindowControllerForWindow:popupWindow];
434  ASSERT_TRUE([popupController isKindOfClass:[BrowserWindowController class]]);
435  EXPECT_FALSE([popupController isTabbedWindow]);
436  EXPECT_FALSE([popupController hasTabStrip]);
437  EXPECT_TRUE([popupController hasTitleBar]);
438  EXPECT_FALSE([popupController isBookmarkBarVisible]);
439  EXPECT_FALSE([popupController hasToolbar]);
440
441  // Open sheet in an application window.
442  [popupController showWindow:nil];
443  alertFrame = [popupController window:popupWindow
444                     willPositionSheet:sheet
445                             usingRect:defaultAlertFrame];
446  EXPECT_EQ(NSMinY(alertFrame),
447            NSHeight([[popupWindow contentView] frame]) -
448            defaultAlertFrame.size.height);
449
450  // Close the application window.
451  popup_browser->tab_strip_model()->CloseSelectedTabs();
452  [popupController close];
453}
454
455// Verify that the info bar tip is hidden when the toolbar is not visible.
456IN_PROC_BROWSER_TEST_F(BrowserWindowControllerTest,
457                       InfoBarTipHiddenForWindowWithoutToolbar) {
458  ShowInfoBar(browser());
459  EXPECT_FALSE(
460      [[controller() infoBarContainerController] shouldSuppressTopInfoBarTip]);
461
462  OpenAppShortcutWindow(browser()->profile(), GURL("about:blank"));
463  Browser* popup_browser = BrowserList::GetInstance(
464      chrome::HOST_DESKTOP_TYPE_NATIVE)->GetLastActive();
465  NSWindow* popupWindow = popup_browser->window()->GetNativeWindow();
466  BrowserWindowController* popupController =
467      [BrowserWindowController browserWindowControllerForWindow:popupWindow];
468  EXPECT_FALSE([popupController hasToolbar]);
469
470  // Show infobar for controller.
471  ShowInfoBar(popup_browser);
472  EXPECT_TRUE(
473      [[popupController infoBarContainerController]
474          shouldSuppressTopInfoBarTip]);
475}
476
477// Tests that status bubble's base frame does move when devTools are docked.
478IN_PROC_BROWSER_TEST_F(BrowserWindowControllerTest,
479                       StatusBubblePositioning) {
480  NSPoint origin = [controller() statusBubbleBaseFrame].origin;
481
482  DevToolsWindow* devtools_window =
483      DevToolsWindowTesting::OpenDevToolsWindowSync(browser(), true);
484  DevToolsWindowTesting::Get(devtools_window)->SetInspectedPageBounds(
485      gfx::Rect(10, 10, 100, 100));
486
487  NSPoint originWithDevTools = [controller() statusBubbleBaseFrame].origin;
488  EXPECT_FALSE(NSEqualPoints(origin, originWithDevTools));
489
490  DevToolsWindowTesting::CloseDevToolsWindowSync(devtools_window);
491}
492
493// Tests that top infobar tip is streched when bookmark bar becomes SHOWN/HIDDEN
494IN_PROC_BROWSER_TEST_F(BrowserWindowControllerTest,
495                       InfoBarTipStretchedWhenBookmarkBarStatusChanged) {
496  EXPECT_FALSE([controller() isBookmarkBarVisible]);
497  ShowInfoBar(browser());
498  // The infobar tip is animated during the infobar is being added, wait until
499  // it completes.
500  WaitForTopInfoBarAnimationToFinish();
501
502  EXPECT_FALSE([[controller() infoBarContainerController]
503      shouldSuppressTopInfoBarTip]);
504
505  NSInteger max_tip_height = infobars::InfoBar::kMaximumArrowTargetHeight +
506      infobars::InfoBar::kSeparatorLineHeight;
507
508  chrome::ExecuteCommand(browser(), IDC_SHOW_BOOKMARK_BAR);
509  WaitForBookmarkBarAnimationToFinish();
510  EXPECT_TRUE([controller() isBookmarkBarVisible]);
511  EXPECT_EQ(std::min(GetExpectedTopInfoBarTipHeight(), max_tip_height),
512            [[controller() infoBarContainerController] overlappingTipHeight]);
513
514  chrome::ExecuteCommand(browser(), IDC_SHOW_BOOKMARK_BAR);
515  WaitForBookmarkBarAnimationToFinish();
516  EXPECT_FALSE([controller() isBookmarkBarVisible]);
517  EXPECT_EQ(std::min(GetExpectedTopInfoBarTipHeight(), max_tip_height),
518            [[controller() infoBarContainerController] overlappingTipHeight]);
519}
520
521IN_PROC_BROWSER_TEST_F(BrowserWindowControllerTest, TrafficLightZOrder) {
522  // Verify z order immediately after creation.
523  VerifyTrafficLightZOrder();
524
525  // Toggle overlay, then verify z order.
526  [controller() showOverlay];
527  [controller() removeOverlay];
528  VerifyTrafficLightZOrder();
529
530  // Toggle immersive fullscreen, then verify z order.
531  [controller() enterImmersiveFullscreen];
532  [controller() exitImmersiveFullscreen];
533  VerifyTrafficLightZOrder();
534}
535