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 | } |