1 // Copyright 2019 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 <cmath>
6 
7 #include "base/optional.h"
8 #include "base/run_loop.h"
9 #include "build/build_config.h"
10 #include "chrome/browser/ui/browser_commands.h"
11 #include "chrome/browser/ui/tabs/tab_strip_model.h"
12 #include "chrome/browser/ui/view_ids.h"
13 #include "chrome/browser/ui/views/frame/app_menu_button.h"
14 #include "chrome/browser/ui/views/frame/browser_non_client_frame_view.h"
15 #include "chrome/browser/ui/views/frame/browser_view.h"
16 #include "chrome/browser/ui/views/frame/toolbar_button_provider.h"
17 #include "chrome/browser/ui/views/page_action/page_action_icon_controller.h"
18 #include "chrome/browser/ui/views/web_apps/web_app_frame_toolbar_test_helper.h"
19 #include "chrome/browser/ui/views/web_apps/web_app_frame_toolbar_view.h"
20 #include "chrome/test/base/in_process_browser_test.h"
21 #include "content/public/browser/web_contents.h"
22 #include "content/public/browser/web_contents_observer.h"
23 #include "content/public/common/page_zoom.h"
24 #include "content/public/test/browser_test.h"
25 #include "content/public/test/browser_test_utils.h"
26 #include "content/public/test/test_utils.h"
27 #include "content/public/test/theme_change_waiter.h"
28 #include "net/test/embedded_test_server/embedded_test_server.h"
29 #include "third_party/skia/include/core/SkColor.h"
30 #include "ui/gfx/geometry/size.h"
31 #include "ui/views/controls/label.h"
32 #include "ui/views/view.h"
33 #include "url/gurl.h"
34 
35 namespace {
36 
37 #if defined(OS_MAC)
38 // Keep in sync with browser_non_client_frame_view_mac.mm
39 constexpr double kTitlePaddingWidthFraction = 0.1;
40 #endif
41 
42 template <typename T>
GetLastVisible(const std::vector<T * > & views)43 T* GetLastVisible(const std::vector<T*>& views) {
44   T* visible = nullptr;
45   for (auto* view : views) {
46     if (view->GetVisible())
47       visible = view;
48   }
49   return visible;
50 }
51 
52 }  // namespace
53 
54 class WebAppFrameToolbarBrowserTest : public InProcessBrowserTest {
55  public:
WebAppFrameToolbarBrowserTest()56   WebAppFrameToolbarBrowserTest()
57       : https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}
58 
https_server()59   net::EmbeddedTestServer* https_server() { return &https_server_; }
60 
61   // InProcessBrowserTest:
SetUp()62   void SetUp() override {
63     https_server_.AddDefaultHandlers(GetChromeTestDataDir());
64 
65     InProcessBrowserTest::SetUp();
66   }
67 
helper()68   WebAppFrameToolbarTestHelper* helper() {
69     return &web_app_frame_toolbar_helper_;
70   }
71 
72  private:
73   net::EmbeddedTestServer https_server_;
74   WebAppFrameToolbarTestHelper web_app_frame_toolbar_helper_;
75 };
76 
IN_PROC_BROWSER_TEST_F(WebAppFrameToolbarBrowserTest,SpaceConstrained)77 IN_PROC_BROWSER_TEST_F(WebAppFrameToolbarBrowserTest, SpaceConstrained) {
78   const GURL app_url("https://test.org");
79   helper()->InstallAndLaunchWebApp(browser(), app_url);
80 
81   views::View* const toolbar_left_container =
82       helper()->web_app_frame_toolbar()->GetLeftContainerForTesting();
83   EXPECT_EQ(toolbar_left_container->parent(),
84             helper()->web_app_frame_toolbar());
85 
86   views::View* const window_title =
87       helper()->frame_view()->GetViewByID(VIEW_ID_WINDOW_TITLE);
88 #if defined(OS_CHROMEOS)
89   EXPECT_FALSE(window_title);
90 #else
91   EXPECT_EQ(window_title->parent(), helper()->frame_view());
92 #endif
93 
94   views::View* const toolbar_right_container =
95       helper()->web_app_frame_toolbar()->GetRightContainerForTesting();
96   EXPECT_EQ(toolbar_right_container->parent(),
97             helper()->web_app_frame_toolbar());
98 
99   std::vector<const PageActionIconView*> page_actions =
100       helper()
101           ->web_app_frame_toolbar()
102           ->GetPageActionIconControllerForTesting()
103           ->GetPageActionIconViewsForTesting();
104   for (const PageActionIconView* action : page_actions)
105     EXPECT_EQ(action->parent(), toolbar_right_container);
106 
107   views::View* const menu_button =
108       helper()->browser_view()->toolbar_button_provider()->GetAppMenuButton();
109   EXPECT_EQ(menu_button->parent(), toolbar_right_container);
110 
111   // Ensure we initially have abundant space.
112   helper()->frame_view()->SetSize(gfx::Size(1000, 1000));
113 
114   EXPECT_TRUE(toolbar_left_container->GetVisible());
115   const int original_left_container_width = toolbar_left_container->width();
116   EXPECT_GT(original_left_container_width, 0);
117 
118 #if defined(OS_WIN) || (defined(OS_LINUX) && !defined(OS_CHROMEOS))
119   const int original_window_title_width = window_title->width();
120   EXPECT_GT(original_window_title_width, 0);
121 #endif
122 
123   // Initially the page action icons are not visible.
124   EXPECT_EQ(GetLastVisible(page_actions), nullptr);
125   const int original_menu_button_width = menu_button->width();
126   EXPECT_GT(original_menu_button_width, 0);
127 
128   // Cause the zoom page action icon to be visible.
129   chrome::Zoom(helper()->app_browser(), content::PAGE_ZOOM_IN);
130 
131   // The layout should be invalidated, but since we don't have the benefit of
132   // the compositor to immediately kick a layout off, we have to do it manually.
133   helper()->frame_view()->Layout();
134 
135   // The page action icons should now take up width, leaving less space on
136   // Windows and Linux for the window title. (On Mac, the window title remains
137   // centered - not tested here.)
138 
139   EXPECT_TRUE(toolbar_left_container->GetVisible());
140   EXPECT_EQ(toolbar_left_container->width(), original_left_container_width);
141 
142 #if defined(OS_WIN) || (defined(OS_LINUX) && !defined(OS_CHROMEOS))
143   EXPECT_GT(window_title->width(), 0);
144   EXPECT_LT(window_title->width(), original_window_title_width);
145 #endif
146 
147   EXPECT_NE(GetLastVisible(page_actions), nullptr);
148   EXPECT_EQ(menu_button->width(), original_menu_button_width);
149 
150   // Resize the WebAppFrameToolbarView just enough to clip out the page action
151   // icons (and toolbar contents left of them).
152   const int original_toolbar_width = helper()->web_app_frame_toolbar()->width();
153   const int new_toolbar_width = toolbar_right_container->width() -
154                                 GetLastVisible(page_actions)->bounds().right();
155   const int new_frame_width = helper()->frame_view()->width() -
156                               original_toolbar_width + new_toolbar_width;
157 
158   helper()->web_app_frame_toolbar()->SetSize(
159       {new_toolbar_width, helper()->web_app_frame_toolbar()->height()});
160   helper()->frame_view()->SetSize(
161       {new_frame_width, helper()->frame_view()->height()});
162 
163   // The left container (containing Back and Reload) should be hidden.
164   EXPECT_FALSE(toolbar_left_container->GetVisible());
165 
166   // The window title should be clipped to 0 width.
167 #if defined(OS_WIN) || (defined(OS_LINUX) && !defined(OS_CHROMEOS))
168   EXPECT_EQ(window_title->width(), 0);
169 #endif
170 
171   // The page action icons should be hidden while the app menu button retains
172   // its full width.
173   EXPECT_EQ(GetLastVisible(page_actions), nullptr);
174   EXPECT_EQ(menu_button->width(), original_menu_button_width);
175 }
176 
IN_PROC_BROWSER_TEST_F(WebAppFrameToolbarBrowserTest,ThemeChange)177 IN_PROC_BROWSER_TEST_F(WebAppFrameToolbarBrowserTest, ThemeChange) {
178   ASSERT_TRUE(https_server()->Start());
179   const GURL app_url = https_server()->GetURL("/banners/theme-color.html");
180   helper()->InstallAndLaunchWebApp(browser(), app_url);
181 
182   content::WebContents* web_contents =
183       helper()->app_browser()->tab_strip_model()->GetActiveWebContents();
184   content::AwaitDocumentOnLoadCompleted(web_contents);
185 
186 #if !defined(OS_LINUX) || defined(OS_CHROMEOS)
187   // Avoid dependence on Linux GTK+ Themes appearance setting.
188 
189   ToolbarButtonProvider* const toolbar_button_provider =
190       helper()->browser_view()->toolbar_button_provider();
191   AppMenuButton* const app_menu_button =
192       toolbar_button_provider->GetAppMenuButton();
193 
194   const SkColor original_ink_drop_color =
195       app_menu_button->GetInkDropBaseColor();
196 
197   {
198     content::ThemeChangeWaiter theme_change_waiter(web_contents);
199     EXPECT_TRUE(content::ExecJs(web_contents,
200                                 "document.getElementById('theme-color')."
201                                 "setAttribute('content', '#246')"));
202     theme_change_waiter.Wait();
203 
204     EXPECT_NE(app_menu_button->GetInkDropBaseColor(), original_ink_drop_color);
205   }
206 
207   {
208     content::ThemeChangeWaiter theme_change_waiter(web_contents);
209     EXPECT_TRUE(content::ExecJs(
210         web_contents, "document.getElementById('theme-color').remove()"));
211     theme_change_waiter.Wait();
212 
213     EXPECT_EQ(app_menu_button->GetInkDropBaseColor(), original_ink_drop_color);
214   }
215 #endif
216 }
217 
218 // Test that a tooltip is shown when hovering over a truncated title.
IN_PROC_BROWSER_TEST_F(WebAppFrameToolbarBrowserTest,TitleHover)219 IN_PROC_BROWSER_TEST_F(WebAppFrameToolbarBrowserTest, TitleHover) {
220   const GURL app_url("https://test.org");
221   helper()->InstallAndLaunchWebApp(browser(), app_url);
222 
223   views::View* const toolbar_left_container =
224       helper()->web_app_frame_toolbar()->GetLeftContainerForTesting();
225   views::View* const toolbar_right_container =
226       helper()->web_app_frame_toolbar()->GetRightContainerForTesting();
227 
228   auto* const window_title = static_cast<views::Label*>(
229       helper()->frame_view()->GetViewByID(VIEW_ID_WINDOW_TITLE));
230 #if defined(OS_CHROMEOS)
231   // Chrome OS PWA windows do not display app titles.
232   EXPECT_EQ(nullptr, window_title);
233   return;
234 #endif
235   EXPECT_EQ(window_title->parent(), helper()->frame_view());
236 
237   window_title->SetText(base::string16(30, 't'));
238 
239   // Ensure we initially have abundant space.
240   helper()->frame_view()->SetSize(gfx::Size(1000, 1000));
241   helper()->frame_view()->Layout();
242   EXPECT_GT(window_title->width(), 0);
243   const int original_title_gap = toolbar_right_container->x() -
244                                  toolbar_left_container->x() -
245                                  toolbar_left_container->width();
246 
247   // With a narrow window, we have insufficient space for the full title.
248   const int narrow_title_gap =
249       window_title->CalculatePreferredSize().width() * 3 / 4;
250   int narrow_frame_width =
251       helper()->frame_view()->width() - original_title_gap + narrow_title_gap;
252 #if defined(OS_MAC)
253   // Increase frame width to allow for title padding.
254   narrow_frame_width = base::checked_cast<int>(
255       std::ceil(narrow_frame_width / (1 - 2 * kTitlePaddingWidthFraction)));
256 #endif
257   helper()->frame_view()->SetSize(gfx::Size(narrow_frame_width, 1000));
258   helper()->frame_view()->Layout();
259 
260   EXPECT_GT(window_title->width(), 0);
261   EXPECT_EQ(window_title->GetTooltipHandlerForPoint(gfx::Point(0, 0)),
262             window_title);
263 
264   EXPECT_EQ(
265       helper()->frame_view()->GetTooltipHandlerForPoint(window_title->origin()),
266       window_title);
267 }
268