1 // Copyright 2014 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 "chrome/browser/ui/webui/signin/login_ui_test_utils.h"
6
7 #include "base/run_loop.h"
8 #include "base/scoped_observer.h"
9 #include "base/strings/stringprintf.h"
10 #include "base/test/bind_test_util.h"
11 #include "base/threading/thread_task_runner_handle.h"
12 #include "base/time/time.h"
13 #include "base/timer/timer.h"
14 #include "build/build_config.h"
15 #include "chrome/browser/profiles/profile.h"
16 #include "chrome/browser/signin/account_consistency_mode_manager.h"
17 #include "chrome/browser/signin/identity_manager_factory.h"
18 #include "chrome/browser/signin/signin_promo.h"
19 #include "chrome/browser/ui/browser.h"
20 #include "chrome/browser/ui/chrome_pages.h"
21 #include "chrome/browser/ui/signin_view_controller_delegate.h"
22 #include "chrome/browser/ui/tabs/tab_strip_model.h"
23 #include "chrome/browser/ui/webui/signin/login_ui_service.h"
24 #include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
25 #include "chrome/browser/ui/webui/signin/signin_utils.h"
26 #include "chrome/test/base/ui_test_utils.h"
27 #include "components/signin/public/identity_manager/identity_manager.h"
28 #include "content/public/browser/notification_service.h"
29 #include "content/public/browser/notification_types.h"
30 #include "content/public/browser/web_contents.h"
31 #include "content/public/test/browser_test_utils.h"
32 #include "content/public/test/test_navigation_observer.h"
33
34 using content::MessageLoopRunner;
35
36 // anonymous namespace for signin with UI helper functions.
37 namespace {
38
39 // When Desktop Identity Consistency (Dice) is enabled, the password field is
40 // not easily accessible on the Gaia page. This script can be used to return it.
41 const char kGetPasswordFieldFromDiceSigninPage[] =
42 "(function() {"
43 " var e = document.getElementById('password');"
44 " if (e == null) return null;"
45 " return e.querySelector('input[type=password]');"
46 "})()";
47
48 // The SignInObserver observes the identity manager and blocks until a signin
49 // success or failure notification is fired.
50 class SignInObserver : public signin::IdentityManager::Observer {
51 public:
SignInObserver()52 SignInObserver() : seen_(false), running_(false), signed_in_(false) {}
53
54 // Returns whether a GoogleSigninSucceeded event has happened.
DidSignIn()55 bool DidSignIn() {
56 return signed_in_;
57 }
58
59 // Blocks and waits until the user signs in. Wait() does not block if a
60 // GoogleSigninSucceeded has already occurred.
Wait()61 void Wait() {
62 if (seen_)
63 return;
64
65 base::OneShotTimer timer;
66 timer.Start(FROM_HERE, base::TimeDelta::FromSeconds(30),
67 base::BindRepeating(&SignInObserver::OnTimeout,
68 base::Unretained(this)));
69 running_ = true;
70 message_loop_runner_ = new MessageLoopRunner;
71 message_loop_runner_->Run();
72 EXPECT_TRUE(seen_);
73 }
74
OnTimeout()75 void OnTimeout() {
76 seen_ = false;
77 if (!running_)
78 return;
79 message_loop_runner_->Quit();
80 running_ = false;
81 FAIL() << "Sign in observer timed out!";
82 }
83
OnPrimaryAccountSet(const CoreAccountInfo & primary_account_info)84 void OnPrimaryAccountSet(
85 const CoreAccountInfo& primary_account_info) override {
86 DVLOG(1) << "Google signin succeeded.";
87 signed_in_ = true;
88 QuitLoopRunner();
89 }
90
QuitLoopRunner()91 void QuitLoopRunner() {
92 seen_ = true;
93 if (!running_)
94 return;
95 message_loop_runner_->Quit();
96 running_ = false;
97 }
98
99 private:
100 // Bool to mark an observed event as seen prior to calling Wait(), used to
101 // prevent the observer from blocking.
102 bool seen_;
103 // True is the message loop runner is running.
104 bool running_;
105 // True if a GoogleSigninSucceeded event has been observed.
106 bool signed_in_;
107 scoped_refptr<MessageLoopRunner> message_loop_runner_;
108 };
109
110 // Synchronously waits for the Sync confirmation to be closed.
111 class SyncConfirmationClosedObserver : public LoginUIService::Observer {
112 public:
WaitForConfirmationClosed()113 void WaitForConfirmationClosed() {
114 if (sync_confirmation_closed_)
115 return;
116
117 base::RunLoop run_loop;
118 quit_closure_ = run_loop.QuitClosure();
119 run_loop.Run();
120 }
121
122 private:
OnSyncConfirmationUIClosed(LoginUIService::SyncConfirmationUIClosedResult result)123 void OnSyncConfirmationUIClosed(
124 LoginUIService::SyncConfirmationUIClosedResult result) override {
125 sync_confirmation_closed_ = true;
126 if (quit_closure_)
127 std::move(quit_closure_).Run();
128 }
129
130 bool sync_confirmation_closed_ = false;
131 base::OnceClosure quit_closure_;
132 };
133
RunLoopFor(base::TimeDelta duration)134 void RunLoopFor(base::TimeDelta duration) {
135 base::RunLoop run_loop;
136 base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
137 FROM_HERE, run_loop.QuitClosure(), duration);
138 run_loop.Run();
139 }
140
141 // Returns the render frame host where Gaia credentials can be filled in.
GetSigninFrame(content::WebContents * web_contents)142 content::RenderFrameHost* GetSigninFrame(content::WebContents* web_contents) {
143 // Dice displays the Gaia page directly in a tab.
144 return web_contents->GetMainFrame();
145 }
146
147 // Waits until the condition is met, by polling.
WaitUntilCondition(const base::RepeatingCallback<bool ()> & condition,const std::string & error_message)148 void WaitUntilCondition(const base::RepeatingCallback<bool()>& condition,
149 const std::string& error_message) {
150 for (int attempt = 0; attempt < 10; ++attempt) {
151 if (condition.Run())
152 return;
153 RunLoopFor(base::TimeDelta::FromMilliseconds(1000));
154 }
155
156 FAIL() << error_message;
157 }
158
159 // Evaluates a boolean script expression in the signin frame.
EvaluateBooleanScriptInSigninFrame(Browser * browser,const std::string & script)160 bool EvaluateBooleanScriptInSigninFrame(Browser* browser,
161 const std::string& script) {
162 content::WebContents* web_contents =
163 browser->tab_strip_model()->GetActiveWebContents();
164 bool result = false;
165 EXPECT_TRUE(content::ExecuteScriptAndExtractBool(
166 GetSigninFrame(web_contents),
167 "window.domAutomationController.send(" + script + ");", &result));
168 return result;
169 }
170
171 // Returns whether an element with id |element_id| exists in the signin page.
ElementExistsByIdInSigninFrame(Browser * browser,const std::string & element_id)172 bool ElementExistsByIdInSigninFrame(Browser* browser,
173 const std::string& element_id) {
174 return EvaluateBooleanScriptInSigninFrame(
175 browser, "document.getElementById('" + element_id + "') != null");
176 }
177
178 // Blocks until an element with an id from |element_ids| exists in the signin
179 // page.
WaitUntilAnyElementExistsInSigninFrame(Browser * browser,const std::vector<std::string> & element_ids)180 void WaitUntilAnyElementExistsInSigninFrame(
181 Browser* browser,
182 const std::vector<std::string>& element_ids) {
183 WaitUntilCondition(
184 base::BindLambdaForTesting([&browser, &element_ids]() -> bool {
185 for (const std::string& element_id : element_ids) {
186 if (ElementExistsByIdInSigninFrame(browser, element_id))
187 return true;
188 }
189 return false;
190 }),
191 "Could not find elements in the signin frame");
192 }
193
194 enum class SyncConfirmationDialogAction { kConfirm, kCancel };
195
196 #if !defined(OS_CHROMEOS)
GetButtonIdForSyncConfirmationDialogAction(SyncConfirmationDialogAction action)197 std::string GetButtonIdForSyncConfirmationDialogAction(
198 SyncConfirmationDialogAction action) {
199 switch (action) {
200 case SyncConfirmationDialogAction::kConfirm:
201 return "confirmButton";
202 case SyncConfirmationDialogAction::kCancel:
203 return "cancelButton";
204 }
205 }
206
GetRadioButtonIdForSigninEmailConfirmationDialogAction(SigninEmailConfirmationDialog::Action action)207 std::string GetRadioButtonIdForSigninEmailConfirmationDialogAction(
208 SigninEmailConfirmationDialog::Action action) {
209 switch (action) {
210 case SigninEmailConfirmationDialog::CREATE_NEW_USER:
211 case SigninEmailConfirmationDialog::CLOSE:
212 return "createNewUserRadioButton";
213 case SigninEmailConfirmationDialog::START_SYNC:
214 return "startSyncRadioButton";
215 }
216 }
217
GetButtonIdForSigninEmailConfirmationDialogAction(SigninEmailConfirmationDialog::Action action)218 std::string GetButtonIdForSigninEmailConfirmationDialogAction(
219 SigninEmailConfirmationDialog::Action action) {
220 switch (action) {
221 case SigninEmailConfirmationDialog::CREATE_NEW_USER:
222 case SigninEmailConfirmationDialog::START_SYNC:
223 return "confirmButton";
224 case SigninEmailConfirmationDialog::CLOSE:
225 return "closeButton";
226 }
227 }
228
GetButtonSelectorForApp(const std::string & app,const std::string & button_id)229 std::string GetButtonSelectorForApp(const std::string& app,
230 const std::string& button_id) {
231 return base::StringPrintf(
232 "(document.querySelector('%s') == null ? null :"
233 "document.querySelector('%s').shadowRoot.querySelector('#%s'))",
234 app.c_str(), app.c_str(), button_id.c_str());
235 }
236 #endif // !defined(OS_CHROMEOS)
237
238 } // namespace
239
240 namespace login_ui_test_utils {
241 class SigninViewControllerTestUtil {
242 public:
TryDismissSyncConfirmationDialog(Browser * browser,SyncConfirmationDialogAction action)243 static bool TryDismissSyncConfirmationDialog(
244 Browser* browser,
245 SyncConfirmationDialogAction action) {
246 #if defined(OS_CHROMEOS)
247 NOTREACHED();
248 return false;
249 #else
250 SigninViewController* signin_view_controller =
251 browser->signin_view_controller();
252 DCHECK(signin_view_controller);
253 if (!signin_view_controller->ShowsModalDialog())
254 return false;
255 content::WebContents* dialog_web_contents =
256 signin_view_controller->GetModalDialogWebContentsForTesting();
257 DCHECK(dialog_web_contents);
258 std::string button_selector = GetButtonSelectorForApp(
259 "sync-confirmation-app",
260 GetButtonIdForSyncConfirmationDialogAction(action));
261 std::string message;
262 std::string find_button_js = base::StringPrintf(
263 "if (document.readyState != 'complete') {"
264 " window.domAutomationController.send('DocumentNotReady');"
265 "} else if (%s == null) {"
266 " window.domAutomationController.send('NotFound');"
267 "} else {"
268 " window.domAutomationController.send('Ok');"
269 "}",
270 button_selector.c_str());
271 EXPECT_TRUE(content::ExecuteScriptAndExtractString(
272 dialog_web_contents, find_button_js, &message));
273 if (message != "Ok")
274 return false;
275
276 // This cannot be a synchronous call, because it closes the window as a side
277 // effect, which may cause the javascript execution to never finish.
278 content::ExecuteScriptAsync(dialog_web_contents,
279 button_selector + ".click();");
280 return true;
281 #endif
282 }
283
TryCompleteSigninEmailConfirmationDialog(Browser * browser,SigninEmailConfirmationDialog::Action action)284 static bool TryCompleteSigninEmailConfirmationDialog(
285 Browser* browser,
286 SigninEmailConfirmationDialog::Action action) {
287 #if defined(OS_CHROMEOS)
288 NOTREACHED();
289 return false;
290 #else
291 SigninViewController* signin_view_controller =
292 browser->signin_view_controller();
293 DCHECK(signin_view_controller);
294 if (!signin_view_controller->ShowsModalDialog())
295 return false;
296 content::WebContents* dialog_web_contents =
297 signin_view_controller->GetModalDialogWebContentsForTesting();
298 DCHECK(dialog_web_contents);
299 std::string radio_button_selector = GetButtonSelectorForApp(
300 "signin-email-confirmation-app",
301 GetRadioButtonIdForSigninEmailConfirmationDialogAction(action));
302 std::string button_selector = GetButtonSelectorForApp(
303 "signin-email-confirmation-app",
304 GetButtonIdForSigninEmailConfirmationDialogAction(action));
305 std::string message;
306 std::string find_button_js = base::StringPrintf(
307 "if (document.readyState != 'complete') {"
308 " window.domAutomationController.send('DocumentNotReady');"
309 "} else if (%s == null || %s == null) {"
310 " window.domAutomationController.send('NotFound');"
311 "} else {"
312 " window.domAutomationController.send('Ok');"
313 "}",
314 radio_button_selector.c_str(), button_selector.c_str());
315 EXPECT_TRUE(content::ExecuteScriptAndExtractString(
316 dialog_web_contents, find_button_js, &message));
317 if (message != "Ok")
318 return false;
319
320 // This cannot be a synchronous call, because it closes the window as a side
321 // effect, which may cause the javascript execution to never finish.
322 content::ExecuteScriptAsync(
323 dialog_web_contents, base::StringPrintf("%s.click(); %s.click();",
324 radio_button_selector.c_str(),
325 button_selector.c_str()));
326 return true;
327 #endif
328 }
329 };
330
WaitUntilUIReady(Browser * browser)331 void WaitUntilUIReady(Browser* browser) {
332 std::string message;
333 ASSERT_TRUE(content::ExecuteScriptAndExtractString(
334 browser->tab_strip_model()->GetActiveWebContents(),
335 "if (!inline.login.getAuthExtHost())"
336 " inline.login.initialize();"
337 "var handler = function() {"
338 " window.domAutomationController.send('ready');"
339 "};"
340 "if (inline.login.isAuthReady())"
341 " handler();"
342 "else"
343 " inline.login.getAuthExtHost().addEventListener('ready', handler);",
344 &message));
345 ASSERT_EQ("ready", message);
346 }
347
SigninInNewGaiaFlow(Browser * browser,const std::string & email,const std::string & password)348 void SigninInNewGaiaFlow(Browser* browser,
349 const std::string& email,
350 const std::string& password) {
351 content::WebContents* web_contents =
352 browser->tab_strip_model()->GetActiveWebContents();
353
354 WaitUntilAnyElementExistsInSigninFrame(browser, {"identifierId"});
355 std::string js = "document.getElementById('identifierId').value = '" + email +
356 "'; document.getElementById('identifierNext').click();";
357 ASSERT_TRUE(content::ExecuteScript(GetSigninFrame(web_contents), js));
358
359 // Fill the password input field.
360 std::string password_script = kGetPasswordFieldFromDiceSigninPage;
361 // Wait until the password field exists.
362 WaitUntilCondition(
363 base::BindLambdaForTesting([&browser, &password_script]() -> bool {
364 return EvaluateBooleanScriptInSigninFrame(browser,
365 password_script + " != null");
366 }),
367 "Could not find Dice password field");
368 js = password_script + ".value = '" + password + "';";
369 js += "document.getElementById('passwordNext').click();";
370 ASSERT_TRUE(content::ExecuteScript(GetSigninFrame(web_contents), js));
371 }
372
SigninInOldGaiaFlow(Browser * browser,const std::string & email,const std::string & password)373 void SigninInOldGaiaFlow(Browser* browser,
374 const std::string& email,
375 const std::string& password) {
376 content::WebContents* web_contents =
377 browser->tab_strip_model()->GetActiveWebContents();
378
379 WaitUntilAnyElementExistsInSigninFrame(browser, {"Email"});
380 std::string js = "document.getElementById('Email').value = '" + email + ";" +
381 "document.getElementById('next').click();";
382 ASSERT_TRUE(content::ExecuteScript(GetSigninFrame(web_contents), js));
383
384 WaitUntilAnyElementExistsInSigninFrame(browser, {"Passwd"});
385 js = "document.getElementById('Passwd').value = '" + password + "';" +
386 "document.getElementById('signIn').click();";
387 ASSERT_TRUE(content::ExecuteScript(GetSigninFrame(web_contents), js));
388 }
389
ExecuteJsToSigninInSigninFrame(Browser * browser,const std::string & email,const std::string & password)390 void ExecuteJsToSigninInSigninFrame(Browser* browser,
391 const std::string& email,
392 const std::string& password) {
393 WaitUntilAnyElementExistsInSigninFrame(browser, {"identifierNext", "next"});
394 if (ElementExistsByIdInSigninFrame(browser, "identifierNext"))
395 SigninInNewGaiaFlow(browser, email, password);
396 else
397 SigninInOldGaiaFlow(browser, email, password);
398 }
399
SignInWithUI(Browser * browser,const std::string & username,const std::string & password)400 bool SignInWithUI(Browser* browser,
401 const std::string& username,
402 const std::string& password) {
403 #if defined(OS_CHROMEOS)
404 NOTREACHED();
405 return false;
406 #else
407 SignInObserver signin_observer;
408 ScopedObserver<signin::IdentityManager, signin::IdentityManager::Observer>
409 scoped_signin_observer(&signin_observer);
410 scoped_signin_observer.Add(
411 IdentityManagerFactory::GetForProfile(browser->profile()));
412
413 signin_metrics::AccessPoint access_point =
414 signin_metrics::AccessPoint::ACCESS_POINT_MENU;
415 chrome::ShowBrowserSignin(browser, access_point);
416 content::WebContents* active_contents =
417 browser->tab_strip_model()->GetActiveWebContents();
418 DCHECK(active_contents);
419 content::TestNavigationObserver observer(
420 active_contents, 1, content::MessageLoopRunner::QuitMode::DEFERRED);
421 observer.Wait();
422 DVLOG(1) << "Sign in user: " << username;
423 ExecuteJsToSigninInSigninFrame(browser, username, password);
424 signin_observer.Wait();
425 return signin_observer.DidSignIn();
426 #endif
427 }
428
DismissSyncConfirmationDialog(Browser * browser,base::TimeDelta timeout,SyncConfirmationDialogAction action)429 bool DismissSyncConfirmationDialog(Browser* browser,
430 base::TimeDelta timeout,
431 SyncConfirmationDialogAction action) {
432 SyncConfirmationClosedObserver confirmation_closed_observer;
433 ScopedObserver<LoginUIService, LoginUIService::Observer>
434 scoped_confirmation_closed_observer(&confirmation_closed_observer);
435 scoped_confirmation_closed_observer.Add(
436 LoginUIServiceFactory::GetForProfile(browser->profile()));
437
438 const base::Time expire_time = base::Time::Now() + timeout;
439 while (base::Time::Now() <= expire_time) {
440 if (SigninViewControllerTestUtil::TryDismissSyncConfirmationDialog(
441 browser, action)) {
442 confirmation_closed_observer.WaitForConfirmationClosed();
443 return true;
444 }
445 RunLoopFor(base::TimeDelta::FromMilliseconds(1000));
446 }
447 return false;
448 }
449
ConfirmSyncConfirmationDialog(Browser * browser,base::TimeDelta timeout)450 bool ConfirmSyncConfirmationDialog(Browser* browser, base::TimeDelta timeout) {
451 return DismissSyncConfirmationDialog(browser, timeout,
452 SyncConfirmationDialogAction::kConfirm);
453 }
454
CancelSyncConfirmationDialog(Browser * browser,base::TimeDelta timeout)455 bool CancelSyncConfirmationDialog(Browser* browser, base::TimeDelta timeout) {
456 return DismissSyncConfirmationDialog(browser, timeout,
457 SyncConfirmationDialogAction::kCancel);
458 }
459
CompleteSigninEmailConfirmationDialog(Browser * browser,base::TimeDelta timeout,SigninEmailConfirmationDialog::Action action)460 bool CompleteSigninEmailConfirmationDialog(
461 Browser* browser,
462 base::TimeDelta timeout,
463 SigninEmailConfirmationDialog::Action action) {
464 const base::Time expire_time = base::Time::Now() + timeout;
465 while (base::Time::Now() <= expire_time) {
466 if (SigninViewControllerTestUtil::TryCompleteSigninEmailConfirmationDialog(
467 browser, action)) {
468 return true;
469 }
470 RunLoopFor(base::TimeDelta::FromMilliseconds(1000));
471 }
472 return false;
473 }
474
475 } // namespace login_ui_test_utils
476