chromium/chrome/android/javatests/src/org/chromium/chrome/browser/jsdialog/JavascriptTabModalDialogTest.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.jsdialog;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isFocusable;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;

import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;

import android.content.pm.ActivityInfo;

import androidx.test.espresso.Espresso;
import androidx.test.filters.MediumTest;

import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Restriction;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.tabmodel.TabClosureParams;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.batch.BlankCTATabInitialStateRule;
import org.chromium.components.javascript_dialogs.JavascriptTabModalDialog;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.test.util.TestCallbackHelperContainer.OnEvaluateJavaScriptResultHelper;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.test.util.UiRestriction;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

/** Test suite for displaying and functioning of tab modal JavaScript alert, confirm and prompt. */
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(JavascriptAppModalDialogTest.JAVASCRIPT_DIALOG_BATCH_NAME)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class JavascriptTabModalDialogTest {
    @ClassRule
    public static ChromeTabbedActivityTestRule sActivityTestRule =
            new ChromeTabbedActivityTestRule();

    @Rule
    public BlankCTATabInitialStateRule mBlankCTATabInitialStateRule =
            new BlankCTATabInitialStateRule(sActivityTestRule, true);

    private static final String EMPTY_PAGE =
            UrlUtils.encodeHtmlDataUri(
                    "<html><title>Modal Dialog Test</title><p>Testcase.</p></title></html>");
    private static final String OTHER_PAGE =
            UrlUtils.encodeHtmlDataUri(
                    "<html><title>Modal Dialog Test</title><p>Testcase. Other"
                            + " tab.</p></title></html>");

    private ChromeTabbedActivity mActivity;

    @Before
    public void setUp() {
        sActivityTestRule.loadUrl(EMPTY_PAGE);
        mActivity = sActivityTestRule.getActivity();
    }

    /**
     * Verifies modal alert-dialog appearance and that JavaScript execution is able to continue
     * after dismissal.
     */
    @Test
    @MediumTest
    @Feature({"Browser", "Main"})
    public void testAlertModalDialog() throws TimeoutException, ExecutionException {
        final OnEvaluateJavaScriptResultHelper scriptEvent =
                executeJavaScriptAndWaitForDialog("alert('Hello Android!');");

        JavascriptTabModalDialog jsDialog = getCurrentDialog();
        Assert.assertNotNull("No dialog showing.", jsDialog);

        onView(withText(R.string.ok)).perform(click());
        Assert.assertTrue(
                "JavaScript execution should continue after closing prompt.",
                scriptEvent.waitUntilHasValue());
    }

    /** Verifies that clicking on a button twice doesn't crash. */
    @Test
    @MediumTest
    @Feature({"Browser", "Main"})
    public void testAlertModalDialogWithTwoClicks() throws TimeoutException, ExecutionException {
        OnEvaluateJavaScriptResultHelper scriptEvent =
                executeJavaScriptAndWaitForDialog("alert('Hello Android');");
        JavascriptTabModalDialog jsDialog = getCurrentDialog();
        Assert.assertNotNull("No dialog showing.", jsDialog);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    PropertyModel model =
                            mActivity.getModalDialogManager().getCurrentDialogForTest();
                    jsDialog.onClick(model, ModalDialogProperties.ButtonType.POSITIVE);
                    jsDialog.onClick(model, ModalDialogProperties.ButtonType.POSITIVE);
                });

        Assert.assertTrue(
                "JavaScript execution should continue after closing prompt.",
                scriptEvent.waitUntilHasValue());
    }

    /**
     * Verifies that modal confirm-dialogs display, two buttons are visible and the return value of
     * [Ok] equals true, [Cancel] equals false.
     */
    @Test
    @MediumTest
    @Feature({"Browser", "Main"})
    public void testConfirmModalDialog() throws TimeoutException, ExecutionException {
        OnEvaluateJavaScriptResultHelper scriptEvent =
                executeJavaScriptAndWaitForDialog("confirm('Android');");

        JavascriptTabModalDialog jsDialog = getCurrentDialog();
        Assert.assertNotNull("No dialog showing.", jsDialog);

        onView(withText(R.string.ok)).check(matches(isDisplayed()));
        onView(withText(R.string.cancel)).check(matches(isDisplayed()));

        onView(withText(R.string.ok)).perform(click());
        Assert.assertTrue(
                "JavaScript execution should continue after closing dialog.",
                scriptEvent.waitUntilHasValue());

        String resultString = scriptEvent.getJsonResultAndClear();
        Assert.assertEquals("Invalid return value.", "true", resultString);

        // Try again, pressing cancel this time.
        scriptEvent = executeJavaScriptAndWaitForDialog("confirm('Android');");
        jsDialog = getCurrentDialog();
        Assert.assertNotNull("No dialog showing.", jsDialog);

        onView(withText(R.string.cancel)).perform(click());
        Assert.assertTrue(
                "JavaScript execution should continue after closing dialog.",
                scriptEvent.waitUntilHasValue());

        resultString = scriptEvent.getJsonResultAndClear();
        Assert.assertEquals("Invalid return value.", "false", resultString);
    }

    /** Verifies that modal prompt-dialogs display and the result is returned. */
    @Test
    @MediumTest
    @Feature({"Browser", "Main"})
    public void testPromptModalDialog() throws TimeoutException, ExecutionException {
        final String promptText = "Hello Android!";
        final OnEvaluateJavaScriptResultHelper scriptEvent =
                executeJavaScriptAndWaitForDialog("prompt('Android', 'default');");

        final JavascriptTabModalDialog jsDialog = getCurrentDialog();
        Assert.assertNotNull("No dialog showing.", jsDialog);

        // Set the text in the prompt field of the dialog.
        onView(withId(R.id.js_modal_dialog_prompt)).perform(replaceText(promptText));

        onView(withText(R.string.ok)).perform(click());
        Assert.assertTrue(
                "JavaScript execution should continue after closing prompt.",
                scriptEvent.waitUntilHasValue());

        String resultString = scriptEvent.getJsonResultAndClear();
        Assert.assertEquals("Invalid return value.", '"' + promptText + '"', resultString);
    }

    /**
     * Verifies that message content in a dialog is only focusable if the message itself is long
     * enough to require scrolling.
     */
    @Test
    @MediumTest
    @Feature({"Browser", "Main"})
    public void testAlertModalDialogMessageFocus() throws TimeoutException, ExecutionException {
        assertScrollViewFocusabilityInAlertDialog("alert('Short message!');", false);

        // Test on landscape mode so that the message is long enough to make scroll view scrollable
        // on a large-screen device.
        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        assertScrollViewFocusabilityInAlertDialog(
                "alert(new Array(200).join('Long message!'));", true);

        // Reset to portrait mode.
        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
    }

    private void assertScrollViewFocusabilityInAlertDialog(
            final String jsAlertScript, final boolean expectedFocusability)
            throws TimeoutException, ExecutionException {
        final OnEvaluateJavaScriptResultHelper scriptEvent =
                executeJavaScriptAndWaitForDialog(jsAlertScript);

        final JavascriptTabModalDialog jsDialog = getCurrentDialog();
        Assert.assertNotNull("No dialog showing.", jsDialog);

        onView(withId(R.id.modal_dialog_title_scroll_view))
                .check(matches(expectedFocusability ? isFocusable() : not(isFocusable())));

        onView(withText(R.string.ok)).perform(click());
        Assert.assertTrue(
                "JavaScript execution should continue after closing prompt.",
                scriptEvent.waitUntilHasValue());
    }

    /**
     * Displays a dialog and closes the tab in the background before attempting to accept the
     * dialog. Verifies that the dialog is dismissed when the tab is closed.
     */
    @Test
    @MediumTest
    @Feature({"Browser", "Main"})
    public void testDialogDismissedAfterClosingTab() {
        executeJavaScriptAndWaitForDialog("alert('Android')");

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivity
                            .getCurrentTabModel()
                            .closeTabs(
                                    TabClosureParams.closeTab(mActivity.getActivityTab())
                                            .allowUndo(false)
                                            .build());
                });

        // Closing the tab should have dismissed the dialog.
        checkDialogShowing("The dialog should have been dismissed when its tab was closed.", false);
    }

    /**
     * Displays a dialog and goes to tab switcher in the before attempting to accept or cancel the
     * dialog. Verifies that the dialog is dismissed.
     */
    @Test
    @MediumTest
    @Feature({"Browser", "Main"})
    @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
    public void testDialogDismissedAfterToggleOverview() {
        executeJavaScriptAndWaitForDialog("alert('Android')");

        onViewWaiting(withId(R.id.tab_switcher_button)).perform(click());

        // Entering tab switcher should have dismissed the dialog.
        checkDialogShowing(
                "The dialog should have been dismissed when switching to overview mode.", false);
    }

    /**
     * Displays a dialog and loads a new URL before attempting to accept or cancel the dialog.
     * Verifies that the dialog is dismissed.
     */
    @Test
    @MediumTest
    @Feature({"Browser", "Main"})
    public void testDialogDismissedAfterUrlUpdated() {
        executeJavaScriptAndWaitForDialog("alert('Android')");

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mActivity
                            .getActivityTab()
                            .loadUrl(new LoadUrlParams(OTHER_PAGE, PageTransition.LINK));
                });

        // Loading a different URL should have dismissed the dialog.
        checkDialogShowing(
                "The dialog should have been dismissed when a new url is loaded.", false);
    }

    /**
     * Displays a dialog and performs back press before attempting to accept or cancel the dialog.
     * Verifies that the dialog is dismissed.
     */
    @Test
    @MediumTest
    @Feature({"Browser", "Main"})
    public void testDialogDismissedAfterBackPressed() {
        executeJavaScriptAndWaitForDialog("alert('Android')");

        Espresso.pressBack();

        // Performing back press should have dismissed the dialog.
        checkDialogShowing("The dialog should have been dismissed after back press.", false);
    }

    /**
     * Asynchronously executes the given code for spawning a dialog and waits for the dialog to be
     * visible.
     */
    private OnEvaluateJavaScriptResultHelper executeJavaScriptAndWaitForDialog(String script) {
        return executeJavaScriptAndWaitForDialog(new OnEvaluateJavaScriptResultHelper(), script);
    }

    /**
     * Given a JavaScript evaluation helper, asynchronously executes the given code for spawning a
     * dialog and waits for the dialog to be visible.
     */
    private OnEvaluateJavaScriptResultHelper executeJavaScriptAndWaitForDialog(
            final OnEvaluateJavaScriptResultHelper helper, String script) {
        helper.evaluateJavaScriptForTests(mActivity.getCurrentWebContents(), script);
        checkDialogShowing("Could not spawn or locate a modal dialog.", true);
        return helper;
    }

    /**
     * Returns the current JavaScript modal dialog showing or null if no such dialog is currently
     * showing.
     */
    private JavascriptTabModalDialog getCurrentDialog() throws ExecutionException {
        return (JavascriptTabModalDialog)
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            PropertyModel model =
                                    mActivity.getModalDialogManager().getCurrentDialogForTest();
                            return model != null
                                    ? model.get(ModalDialogProperties.CONTROLLER)
                                    : null;
                        });
    }

    /** Check whether dialog is showing as expected. */
    private void checkDialogShowing(final String errorMessage, final boolean shouldBeShown) {
        CriteriaHelper.pollUiThread(
                () -> {
                    final boolean isShown = mActivity.getModalDialogManager().isShowing();
                    Criteria.checkThat(errorMessage, isShown, is(shouldBeShown));
                });
    }
}