| 1 | // Copyright (c) 2013 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 | package org.chromium.chrome.browser.test; |
| 6 | |
| 7 | import android.app.AlertDialog; |
| 8 | import android.content.DialogInterface; |
| 9 | import android.test.suitebuilder.annotation.MediumTest; |
| 10 | import android.util.Log; |
| 11 | import android.view.View; |
| 12 | import android.widget.Button; |
| 13 | import android.widget.CheckBox; |
| 14 | import android.widget.EditText; |
| 15 | |
| 16 | import org.chromium.base.ThreadUtils; |
| 17 | import org.chromium.base.test.util.DisabledTest; |
| 18 | import org.chromium.base.test.util.Feature; |
| 19 | import org.chromium.base.test.util.UrlUtils; |
| 20 | import org.chromium.chrome.browser.JavascriptAppModalDialog; |
| 21 | import org.chromium.chrome.testshell.ChromiumTestShellTestBase; |
| 22 | import org.chromium.chrome.testshell.TabShellTabUtils; |
| 23 | import org.chromium.chrome.testshell.TabShellTabUtils.TestCallbackHelperContainerForTab; |
| 24 | import org.chromium.content.browser.test.util.Criteria; |
| 25 | import org.chromium.content.browser.test.util.CriteriaHelper; |
| 26 | import org.chromium.content.browser.test.util.TestCallbackHelperContainer; |
| 27 | import org.chromium.content.browser.test.util.TestCallbackHelperContainer.OnEvaluateJavaScriptResultHelper; |
| 28 | |
| 29 | import java.util.concurrent.Callable; |
| 30 | import java.util.concurrent.ExecutionException; |
| 31 | import java.util.concurrent.TimeoutException; |
| 32 | |
| 33 | /** |
| 34 | * Test suite for displaying and functioning of modal dialogs. |
| 35 | */ |
| 36 | public class ModalDialogTest extends ChromiumTestShellTestBase { |
| 37 | private final static String TAG = "ModalDialogTest"; |
| 38 | private final static String EMPTY_PAGE = UrlUtils.encodeHtmlDataUri( |
| 39 | "<html><title>Modal Dialog Test</title><p>Testcase.</p></title></html>"); |
| 40 | private final static String BEFORE_UNLOAD_URL = UrlUtils.encodeHtmlDataUri( |
| 41 | "<html>" + |
| 42 | "<head><script>window.onbeforeunload=function() {" + |
| 43 | "return 'Are you sure?';" + |
| 44 | "};</script></head></html>"); |
| 45 | |
| 46 | @Override |
| 47 | public void setUp() throws Exception { |
| 48 | super.setUp(); |
| 49 | launchChromiumTestShellWithUrl(EMPTY_PAGE); |
| 50 | assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading()); |
| 51 | } |
| 52 | |
| 53 | /** |
| 54 | * Verifies modal alert-dialog appearance and that JavaScript execution is |
| 55 | * able to continue after dismissal. |
| 56 | */ |
| 57 | @MediumTest |
| 58 | @Feature({"Browser", "Main"}) |
| 59 | public void testAlertModalDialog() |
| 60 | throws InterruptedException, TimeoutException, ExecutionException { |
| 61 | final OnEvaluateJavaScriptResultHelper scriptEvent = |
| 62 | executeJavaScriptAndWaitForDialog("alert('Hello Android!');"); |
| 63 | |
| 64 | JavascriptAppModalDialog jsDialog = getCurrentDialog(); |
| 65 | assertNotNull("No dialog showing.", jsDialog); |
| 66 | |
| 67 | clickOk(jsDialog); |
| 68 | assertTrue("JavaScript execution should continue after closing prompt.", |
| 69 | scriptEvent.waitUntilHasValue()); |
| 70 | } |
| 71 | |
| 72 | /** |
| 73 | * Verifies that clicking on a button twice doesn't crash. |
| 74 | */ |
| 75 | @MediumTest |
| 76 | @Feature({"Browser", "Main"}) |
| 77 | public void testAlertModalDialogWithTwoClicks() |
| 78 | throws InterruptedException, TimeoutException, ExecutionException { |
| 79 | OnEvaluateJavaScriptResultHelper scriptEvent = |
| 80 | executeJavaScriptAndWaitForDialog("alert('Hello Android');"); |
| 81 | JavascriptAppModalDialog jsDialog = getCurrentDialog(); |
| 82 | assertNotNull("No dialog showing.", jsDialog); |
| 83 | |
| 84 | clickOk(jsDialog); |
| 85 | clickOk(jsDialog); |
| 86 | |
| 87 | assertTrue("JavaScript execution should continue after closing prompt.", |
| 88 | scriptEvent.waitUntilHasValue()); |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * Verifies that modal confirm-dialogs display, two buttons are visible and |
| 93 | * the return value of [Ok] equals true, [Cancel] equals false. |
| 94 | */ |
| 95 | @MediumTest |
| 96 | @Feature({"Browser", "Main"}) |
| 97 | public void testConfirmModalDialog() |
| 98 | throws InterruptedException, TimeoutException, ExecutionException { |
| 99 | OnEvaluateJavaScriptResultHelper scriptEvent = |
| 100 | executeJavaScriptAndWaitForDialog("confirm('Android');"); |
| 101 | |
| 102 | JavascriptAppModalDialog jsDialog = getCurrentDialog(); |
| 103 | assertNotNull("No dialog showing.", jsDialog); |
| 104 | |
| 105 | Button[] buttons = getAlertDialogButtons(jsDialog.getDialogForTest()); |
| 106 | assertNotNull("No cancel button in confirm dialog.", buttons[0]); |
| 107 | assertEquals("Cancel button is not visible.", View.VISIBLE, buttons[0].getVisibility()); |
| 108 | if (buttons[1] != null) { |
| 109 | assertNotSame("Neutral button visible when it should not.", |
| 110 | View.VISIBLE, buttons[1].getVisibility()); |
| 111 | } |
| 112 | assertNotNull("No OK button in confirm dialog.", buttons[2]); |
| 113 | assertEquals("OK button is not visible.", View.VISIBLE, buttons[2].getVisibility()); |
| 114 | |
| 115 | clickOk(jsDialog); |
| 116 | assertTrue("JavaScript execution should continue after closing dialog.", |
| 117 | scriptEvent.waitUntilHasValue()); |
| 118 | |
| 119 | String resultString = scriptEvent.getJsonResultAndClear(); |
| 120 | assertEquals("Invalid return value.", "true", resultString); |
| 121 | |
| 122 | // Try again, pressing cancel this time. |
| 123 | scriptEvent = executeJavaScriptAndWaitForDialog("confirm('Android');"); |
| 124 | jsDialog = getCurrentDialog(); |
| 125 | assertNotNull("No dialog showing.", jsDialog); |
| 126 | |
| 127 | clickCancel(jsDialog); |
| 128 | assertTrue("JavaScript execution should continue after closing dialog.", |
| 129 | scriptEvent.waitUntilHasValue()); |
| 130 | |
| 131 | resultString = scriptEvent.getJsonResultAndClear(); |
| 132 | assertEquals("Invalid return value.", "false", resultString); |
| 133 | } |
| 134 | |
| 135 | /** |
| 136 | * Verifies that modal prompt-dialogs display and the result is returned. |
| 137 | */ |
| 138 | @MediumTest |
| 139 | @Feature({"Browser", "Main"}) |
| 140 | public void testPromptModalDialog() |
| 141 | throws InterruptedException, TimeoutException, ExecutionException { |
| 142 | final String promptText = "Hello Android!"; |
| 143 | final OnEvaluateJavaScriptResultHelper scriptEvent = |
| 144 | executeJavaScriptAndWaitForDialog("prompt('Android', 'default');"); |
| 145 | |
| 146 | final JavascriptAppModalDialog jsDialog = getCurrentDialog(); |
| 147 | assertNotNull("No dialog showing.", jsDialog); |
| 148 | |
| 149 | // Set the text in the prompt field of the dialog. |
| 150 | boolean result = ThreadUtils.runOnUiThreadBlocking(new Callable<Boolean>() { |
| 151 | @Override |
| 152 | public Boolean call() { |
| 153 | EditText prompt = (EditText) jsDialog.getDialogForTest().findViewById( |
| 154 | org.chromium.chrome.R.id.js_modal_dialog_prompt); |
| 155 | if (prompt == null) return false; |
| 156 | prompt.setText(promptText); |
| 157 | return true; |
| 158 | } |
| 159 | }); |
| 160 | assertTrue("Failed to find prompt view in prompt dialog.", result); |
| 161 | |
| 162 | clickOk(jsDialog); |
| 163 | assertTrue("JavaScript execution should continue after closing prompt.", |
| 164 | scriptEvent.waitUntilHasValue()); |
| 165 | |
| 166 | String resultString = scriptEvent.getJsonResultAndClear(); |
| 167 | assertEquals("Invalid return value.", '"' + promptText + '"', resultString); |
| 168 | } |
| 169 | |
| 170 | /** |
| 171 | * Verifies beforeunload dialogs are shown and they block/allow navigation |
| 172 | * as appropriate. |
| 173 | */ |
| 174 | //@MediumTest |
| 175 | //@Feature({"Browser", "Main"}) |
| 176 | @DisabledTest //crbug/270593 |
| 177 | public void testBeforeUnloadDialog() |
| 178 | throws InterruptedException, TimeoutException, ExecutionException { |
| 179 | loadUrlWithSanitization(BEFORE_UNLOAD_URL); |
| 180 | executeJavaScriptAndWaitForDialog("history.back();"); |
| 181 | |
| 182 | JavascriptAppModalDialog jsDialog = getCurrentDialog(); |
| 183 | assertNotNull("No dialog showing.", jsDialog); |
| 184 | checkButtonPresenceVisibilityText( |
| 185 | jsDialog, 0, org.chromium.chrome.R.string.stay_on_this_page, |
| 186 | "Stay on this page"); |
| 187 | clickCancel(jsDialog); |
| 188 | |
| 189 | assertEquals(BEFORE_UNLOAD_URL, getActivity().getActiveContentView().getUrl()); |
| 190 | executeJavaScriptAndWaitForDialog("history.back();"); |
| 191 | |
| 192 | jsDialog = getCurrentDialog(); |
| 193 | assertNotNull("No dialog showing.", jsDialog); |
| 194 | checkButtonPresenceVisibilityText( |
| 195 | jsDialog, 2, org.chromium.chrome.R.string.leave_this_page, |
| 196 | "Leave this page"); |
| 197 | |
| 198 | final TestCallbackHelperContainer.OnPageFinishedHelper onPageLoaded = |
| 199 | getActiveTabTestCallbackHelperContainer().getOnPageFinishedHelper(); |
| 200 | int callCount = onPageLoaded.getCallCount(); |
| 201 | clickOk(jsDialog); |
| 202 | onPageLoaded.waitForCallback(callCount); |
| 203 | assertEquals(EMPTY_PAGE, getActivity().getActiveContentView().getUrl()); |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * Verifies that when showing a beforeunload dialogs as a result of a page |
| 208 | * reload, the correct UI strings are used. |
| 209 | */ |
| 210 | @MediumTest |
| 211 | @Feature({"Browser", "Main"}) |
| 212 | public void testBeforeUnloadOnReloadDialog() |
| 213 | throws InterruptedException, TimeoutException, ExecutionException { |
| 214 | loadUrlWithSanitization(BEFORE_UNLOAD_URL); |
| 215 | executeJavaScriptAndWaitForDialog("window.location.reload();"); |
| 216 | |
| 217 | JavascriptAppModalDialog jsDialog = getCurrentDialog(); |
| 218 | assertNotNull("No dialog showing.", jsDialog); |
| 219 | |
| 220 | checkButtonPresenceVisibilityText( |
| 221 | jsDialog, 0, org.chromium.chrome.R.string.dont_reload_this_page, |
| 222 | "Don't reload this page"); |
| 223 | checkButtonPresenceVisibilityText( |
| 224 | jsDialog, 2, org.chromium.chrome.R.string.reload_this_page, |
| 225 | "Reload this page"); |
| 226 | } |
| 227 | |
| 228 | /** |
| 229 | * Verifies that repeated dialogs give the option to disable dialogs |
| 230 | * altogether and then that disabling them works. |
| 231 | */ |
| 232 | @MediumTest |
| 233 | @Feature({"Browser", "Main"}) |
| 234 | public void testDisableRepeatedDialogs() |
| 235 | throws InterruptedException, TimeoutException, ExecutionException { |
| 236 | OnEvaluateJavaScriptResultHelper scriptEvent = |
| 237 | executeJavaScriptAndWaitForDialog("alert('Android');"); |
| 238 | |
| 239 | // Show a dialog once. |
| 240 | JavascriptAppModalDialog jsDialog = getCurrentDialog(); |
| 241 | assertNotNull("No dialog showing.", jsDialog); |
| 242 | |
| 243 | clickCancel(jsDialog); |
| 244 | scriptEvent.waitUntilHasValue(); |
| 245 | |
| 246 | // Show it again, it should have the option to suppress subsequent dialogs. |
| 247 | scriptEvent = executeJavaScriptAndWaitForDialog("alert('Android');"); |
| 248 | jsDialog = getCurrentDialog(); |
| 249 | assertNotNull("No dialog showing.", jsDialog); |
| 250 | final AlertDialog dialog = jsDialog.getDialogForTest(); |
| 251 | String errorMessage = ThreadUtils.runOnUiThreadBlocking(new Callable<String>() { |
| 252 | @Override |
| 253 | public String call() { |
| 254 | final CheckBox suppress = (CheckBox) dialog.findViewById( |
| 255 | org.chromium.chrome.R.id.suppress_js_modal_dialogs); |
| 256 | if (suppress == null) return "Suppress checkbox not found."; |
| 257 | if (suppress.getVisibility() != View.VISIBLE) { |
| 258 | return "Suppress checkbox is not visible."; |
| 259 | } |
| 260 | suppress.setChecked(true); |
| 261 | return null; |
| 262 | } |
| 263 | }); |
| 264 | assertNull(errorMessage, errorMessage); |
| 265 | clickCancel(jsDialog); |
| 266 | scriptEvent.waitUntilHasValue(); |
| 267 | |
| 268 | scriptEvent.evaluateJavaScript(getActivity().getActiveContentView().getContentViewCore(), |
| 269 | "alert('Android');"); |
| 270 | assertTrue("No further dialog boxes should be shown.", scriptEvent.waitUntilHasValue()); |
| 271 | } |
| 272 | |
| 273 | /** |
| 274 | * Displays a dialog and closes the tab in the background before attempting |
| 275 | * to accept the dialog. Verifies that the dialog is dismissed when the tab |
| 276 | * is closed. |
| 277 | */ |
| 278 | @MediumTest |
| 279 | @Feature({"Browser", "Main"}) |
| 280 | public void testDialogDismissedAfterClosingTab() |
| 281 | throws InterruptedException, TimeoutException, ExecutionException { |
| 282 | executeJavaScriptAndWaitForDialog("alert('Android')"); |
| 283 | |
| 284 | final TestCallbackHelperContainerForTab.OnCloseTabHelper onTabClosed = |
| 285 | getActiveTabTestCallbackHelperContainer().getOnCloseTabHelper(); |
| 286 | int callCount = onTabClosed.getCallCount(); |
| 287 | ThreadUtils.runOnUiThreadBlocking(new Runnable() { |
| 288 | @Override |
| 289 | public void run() { |
| 290 | getActivity().createTab(EMPTY_PAGE); |
| 291 | } |
| 292 | }); |
| 293 | onTabClosed.waitForCallback(callCount); |
| 294 | |
| 295 | // Closing the tab should have dismissed the dialog. |
| 296 | boolean criteriaSatisfied = CriteriaHelper.pollForCriteria( |
| 297 | new JavascriptAppModalDialogShownCriteria(false)); |
| 298 | assertTrue("The dialog should have been dismissed when its tab was closed.", |
| 299 | criteriaSatisfied); |
| 300 | } |
| 301 | |
| 302 | /** |
| 303 | * Asynchronously executes the given code for spawning a dialog and waits |
| 304 | * for the dialog to be visible. |
| 305 | */ |
| 306 | private OnEvaluateJavaScriptResultHelper executeJavaScriptAndWaitForDialog(String script) |
| 307 | throws InterruptedException { |
| 308 | final OnEvaluateJavaScriptResultHelper helper = |
| 309 | getActiveTabTestCallbackHelperContainer().getOnEvaluateJavaScriptResultHelper(); |
| 310 | return executeJavaScriptAndWaitForDialog(helper, script); |
| 311 | } |
| 312 | |
| 313 | /** |
| 314 | * Given a JavaScript evaluation helper, asynchronously executes the given |
| 315 | * code for spawning a dialog and waits for the dialog to be visible. |
| 316 | */ |
| 317 | private OnEvaluateJavaScriptResultHelper executeJavaScriptAndWaitForDialog( |
| 318 | final OnEvaluateJavaScriptResultHelper helper, String script) |
| 319 | throws InterruptedException { |
| 320 | helper.evaluateJavaScript(getActivity().getActiveContentView().getContentViewCore(), |
| 321 | script); |
| 322 | boolean criteriaSatisfied = CriteriaHelper.pollForCriteria( |
| 323 | new JavascriptAppModalDialogShownCriteria(true)); |
| 324 | assertTrue("Could not spawn or locate a modal dialog.", criteriaSatisfied); |
| 325 | return helper; |
| 326 | } |
| 327 | |
| 328 | /** |
| 329 | * Returns an array of the 3 buttons for this dialog, in the order |
| 330 | * BUTTON_NEGATIVE, BUTTON_NEUTRAL and BUTTON_POSITIVE. Any of these values |
| 331 | * can be null. |
| 332 | */ |
| 333 | private Button[] getAlertDialogButtons(final AlertDialog dialog) throws ExecutionException { |
| 334 | return ThreadUtils.runOnUiThreadBlocking(new Callable<Button[]>() { |
| 335 | @Override |
| 336 | public Button[] call() { |
| 337 | final Button[] buttons = new Button[3]; |
| 338 | buttons[0] = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); |
| 339 | buttons[1] = dialog.getButton(DialogInterface.BUTTON_NEUTRAL); |
| 340 | buttons[2] = dialog.getButton(DialogInterface.BUTTON_POSITIVE); |
| 341 | return buttons; |
| 342 | } |
| 343 | }); |
| 344 | } |
| 345 | |
| 346 | /** |
| 347 | * Returns the current JavaScript modal dialog showing or null if no such dialog is currently |
| 348 | * showing. |
| 349 | */ |
| 350 | private JavascriptAppModalDialog getCurrentDialog() throws ExecutionException { |
| 351 | return ThreadUtils.runOnUiThreadBlocking(new Callable<JavascriptAppModalDialog>() { |
| 352 | @Override |
| 353 | public JavascriptAppModalDialog call() { |
| 354 | return JavascriptAppModalDialog.getCurrentDialogForTest(); |
| 355 | } |
| 356 | }); |
| 357 | } |
| 358 | |
| 359 | private static class JavascriptAppModalDialogShownCriteria implements Criteria { |
| 360 | private final boolean mShouldBeShown; |
| 361 | |
| 362 | public JavascriptAppModalDialogShownCriteria(boolean shouldBeShown) { |
| 363 | mShouldBeShown = shouldBeShown; |
| 364 | } |
| 365 | |
| 366 | @Override |
| 367 | public boolean isSatisfied() { |
| 368 | try { |
| 369 | return ThreadUtils.runOnUiThreadBlocking(new Callable<Boolean>() { |
| 370 | @Override |
| 371 | public Boolean call() { |
| 372 | final boolean isShown = |
| 373 | JavascriptAppModalDialog.getCurrentDialogForTest() != null; |
| 374 | return mShouldBeShown == isShown; |
| 375 | } |
| 376 | }); |
| 377 | } catch (ExecutionException e) { |
| 378 | Log.e(TAG, "Failed to getCurrentDialog", e); |
| 379 | return false; |
| 380 | } |
| 381 | } |
| 382 | } |
| 383 | |
| 384 | /** |
| 385 | * Simulates pressing the OK button of the passed dialog. |
| 386 | */ |
| 387 | private void clickOk(JavascriptAppModalDialog dialog) { |
| 388 | clickButton(dialog, DialogInterface.BUTTON_POSITIVE); |
| 389 | } |
| 390 | |
| 391 | /** |
| 392 | * Simulates pressing the Cancel button of the passed dialog. |
| 393 | */ |
| 394 | private void clickCancel(JavascriptAppModalDialog dialog) { |
| 395 | clickButton(dialog, DialogInterface.BUTTON_NEGATIVE); |
| 396 | } |
| 397 | |
| 398 | private void clickButton(final JavascriptAppModalDialog dialog, final int whichButton) { |
| 399 | ThreadUtils.runOnUiThreadBlocking(new Runnable() { |
| 400 | @Override |
| 401 | public void run() { |
| 402 | dialog.onClick(null, whichButton); |
| 403 | } |
| 404 | }); |
| 405 | } |
| 406 | |
| 407 | private void checkButtonPresenceVisibilityText( |
| 408 | JavascriptAppModalDialog jsDialog, int buttonIndex, |
| 409 | int expectedTextResourceId, String readableName) throws ExecutionException { |
| 410 | final Button[] buttons = getAlertDialogButtons(jsDialog.getDialogForTest()); |
| 411 | final Button button = buttons[buttonIndex]; |
| 412 | assertNotNull("No '" + readableName + "' button in confirm dialog.", button); |
| 413 | assertEquals("'" + readableName + "' button is not visible.", |
| 414 | View.VISIBLE, |
| 415 | button.getVisibility()); |
| 416 | assertEquals("'" + readableName + "' button has wrong text", |
| 417 | getActivity().getResources().getString(expectedTextResourceId), |
| 418 | button.getText().toString()); |
| 419 | } |
| 420 | |
| 421 | private TestCallbackHelperContainerForTab getActiveTabTestCallbackHelperContainer() { |
| 422 | return TabShellTabUtils.getTestCallbackHelperContainer(getActivity().getActiveTab()); |
| 423 | } |
| 424 | } |