chromium/chrome/android/javatests/src/org/chromium/chrome/browser/webapps/AddToHomescreenAddShortcutTest.java

// Copyright 2015 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.webapps;

import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Handler;
import android.text.TextUtils;

import androidx.test.filters.SmallTest;

import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
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.MinAndroidSdkLevel;
import org.chromium.base.test.util.Restriction;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.chrome.browser.ShortcutHelper;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.banners.AppMenuVerbiage;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.browser.TabLoadObserver;
import org.chromium.chrome.test.util.browser.TabTitleObserver;
import org.chromium.chrome.test.util.browser.webapps.WebappTestPage;
import org.chromium.components.webapps.AddToHomescreenCoordinator;
import org.chromium.components.webapps.AddToHomescreenDialogView;
import org.chromium.components.webapps.AddToHomescreenProperties;
import org.chromium.components.webapps.AddToHomescreenViewDelegate;
import org.chromium.components.webapps.AppType;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.common.ContentSwitches;
import org.chromium.net.test.EmbeddedTestServerRule;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.test.util.DeviceRestriction;

/**
 * Tests org.chromium.chrome.browser.webapps.addtohomescreen.AddToHomescreenManager and its C++
 * counterpart.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@Restriction(DeviceRestriction.RESTRICTION_TYPE_NON_AUTO)
public class AddToHomescreenAddShortcutTest {
    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    @Rule public EmbeddedTestServerRule mTestServerRule = new EmbeddedTestServerRule();

    private static final String WEBAPP_ACTION_NAME = "WEBAPP_ACTION";

    private static final String WEBAPP_TITLE = "Webapp shortcut";
    private static final String WEBAPP_HTML =
            UrlUtils.encodeHtmlDataUri(
                    "<html><head>"
                            + "<meta name=\"mobile-web-app-capable\" content=\"yes\" />"
                            + "<title>"
                            + WEBAPP_TITLE
                            + "</title>"
                            + "</head><body>Webapp capable</body></html>");
    private static final String EDITED_WEBAPP_TITLE = "Webapp shortcut edited";

    private static final String SECOND_WEBAPP_TITLE = "Webapp shortcut #2";
    private static final String SECOND_WEBAPP_HTML =
            UrlUtils.encodeHtmlDataUri(
                    "<html><head>"
                            + "<meta name=\"mobile-web-app-capable\" content=\"yes\" />"
                            + "<title>"
                            + SECOND_WEBAPP_TITLE
                            + "</title>"
                            + "</head><body>Webapp capable again</body></html>");

    private static final String NORMAL_TITLE = "Plain shortcut";
    private static final String NORMAL_HTML =
            UrlUtils.encodeHtmlDataUri(
                    "<html>"
                            + "<head><title>"
                            + NORMAL_TITLE
                            + "</title></head>"
                            + "<body>Not Webapp capable</body></html>");

    private static final String META_APP_NAME_PAGE_TITLE = "Not the right title";
    private static final String META_APP_NAME_TITLE = "Web application-name";
    private static final String META_APP_NAME_HTML =
            UrlUtils.encodeHtmlDataUri(
                    "<html><head>"
                            + "<meta name=\"mobile-web-app-capable\" content=\"yes\" />"
                            + "<meta name=\"application-name\" content=\""
                            + META_APP_NAME_TITLE
                            + "\">"
                            + "<title>"
                            + META_APP_NAME_PAGE_TITLE
                            + "</title>"
                            + "</head><body>Webapp capable</body></html>");

    private static final String NON_MASKABLE_MANIFEST_TEST_PAGE_PATH =
            "/chrome/test/data/banners/manifest_test_page.html";
    private static final String MASKABLE_MANIFEST_TEST_PAGE_PATH =
            "/chrome/test/data/banners/manifest_test_page.html?manifest=manifest_maskable.json";
    private static final String MANIFEST_TEST_PAGE_TITLE = "Web app banner test page";

    private static class TestShortcutHelperDelegate extends ShortcutHelper.Delegate {
        public String mRequestedShortcutTitle;
        public Intent mRequestedShortcutIntent;
        public boolean mRequestedShortcutAdaptable;

        @Override
        public void addShortcutToHomescreen(
                String id,
                String title,
                Bitmap icon,
                boolean iconAdaptable,
                Intent shortcutIntent) {
            mRequestedShortcutTitle = title;
            mRequestedShortcutIntent = shortcutIntent;
            mRequestedShortcutAdaptable = iconAdaptable;
        }

        @Override
        public String getFullscreenAction() {
            return WEBAPP_ACTION_NAME;
        }

        public void clearRequestedShortcutData() {
            mRequestedShortcutTitle = null;
            mRequestedShortcutIntent = null;
            mRequestedShortcutAdaptable = false;
        }
    }

    /**
     * Test TestAddToHomescreenCoordinator subclass which mocks showing the add-to-homescreen view
     * and adds the shortcut to the home screen once it is ready.
     */
    private static class TestAddToHomescreenCoordinator extends AddToHomescreenCoordinator {
        private String mTitle;

        // The type of of dialog expected to show (at the time of submission).
        private @AppType int mExpectedDialogType;

        TestAddToHomescreenCoordinator(
                WebContents webContents,
                Context context,
                WindowAndroid windowAndroid,
                ModalDialogManager modalDialogManager,
                String title,
                @AppType int expectedDialogType) {
            super(webContents, context, windowAndroid, modalDialogManager);
            mTitle = title;
            mExpectedDialogType = expectedDialogType;
        }

        @Override
        protected AddToHomescreenDialogView initView(AddToHomescreenViewDelegate delegate) {
            return new AddToHomescreenDialogView(
                    getContextForTests(), getModalDialogManagerForTests(), delegate) {
                @Override
                protected void setTitle(String title) {
                    if (TextUtils.isEmpty(mTitle)) {
                        mTitle = title;
                    }
                }

                @Override
                protected void setCanSubmit(boolean canSubmit) {
                    Assert.assertEquals(
                            mExpectedDialogType,
                            getPropertyModelForTesting().get(AddToHomescreenProperties.TYPE));

                    // Submit the dialog.
                    new Handler().post(() -> mDelegate.onAddToHomescreen(mTitle, AppType.SHORTCUT));
                }
            };
        }
    }

    private ChromeActivity mActivity;
    private Tab mTab;
    private TestShortcutHelperDelegate mShortcutHelperDelegate;

    @Before
    public void setUp() throws Exception {
        mActivityTestRule.startMainActivityOnBlankPage();
        mShortcutHelperDelegate = new TestShortcutHelperDelegate();
        ShortcutHelper.setDelegateForTests(mShortcutHelperDelegate);
        mActivity = mActivityTestRule.getActivity();
        mTab = mActivity.getActivityTab();
    }

    @Test
    @SmallTest
    @Feature("{Webapp}")
    public void testAddToHomescreenForWebappCreatesShortcut() throws Exception {
        // This test attempts to create a shortcut for something the installability pipeline sees as
        // a web app and would, under normal circumstances, install a webapk, but because universal
        // install is in play, a shortcut gets created. If the universal install flag is disabled,
        // the assert in canSubmit (above) fires.
        loadUrl(
                WebappTestPage.getServiceWorkerUrl(mTestServerRule.getServer()),
                WebappTestPage.PAGE_TITLE);
        addShortcutToTab(mTab, "", true, /* expectedDialogType= */ AppType.SHORTCUT);
    }

    @Test
    @SmallTest
    @Feature("{Webapp}")
    public void testAddWebappShortcuts() throws Exception {
        // Add a webapp shortcut and make sure the intent's parameters make sense.
        loadUrl(WEBAPP_HTML, WEBAPP_TITLE);
        addShortcutToTab(mTab, "", true, /* expectedDialogType= */ AppType.SHORTCUT);
        Assert.assertEquals(WEBAPP_TITLE, mShortcutHelperDelegate.mRequestedShortcutTitle);

        Intent launchIntent = mShortcutHelperDelegate.mRequestedShortcutIntent;
        Assert.assertEquals(mActivity.getPackageName(), launchIntent.getPackage());
        Assert.assertEquals(Intent.ACTION_VIEW, launchIntent.getAction());
        Assert.assertEquals(WEBAPP_HTML, launchIntent.getDataString());

        // Add a second shortcut and make sure it matches the second webapp's
        // parameters.
        mShortcutHelperDelegate.clearRequestedShortcutData();
        loadUrl(SECOND_WEBAPP_HTML, SECOND_WEBAPP_TITLE);
        addShortcutToTab(mTab, "", true, /* expectedDialogType= */ AppType.SHORTCUT);
        Assert.assertEquals(SECOND_WEBAPP_TITLE, mShortcutHelperDelegate.mRequestedShortcutTitle);

        Intent newLaunchIntent = mShortcutHelperDelegate.mRequestedShortcutIntent;
        Assert.assertEquals(mActivity.getPackageName(), newLaunchIntent.getPackage());
        Assert.assertEquals(Intent.ACTION_VIEW, newLaunchIntent.getAction());
        Assert.assertEquals(SECOND_WEBAPP_HTML, newLaunchIntent.getDataString());
    }

    @Test
    @SmallTest
    @Feature("{Webapp}")
    @MinAndroidSdkLevel(Build.VERSION_CODES.O)
    public void testAddAdaptableShortcut() throws Exception {
        // Test the baseline of no adaptive icon.
        loadUrl(
                mTestServerRule.getServer().getURL(NON_MASKABLE_MANIFEST_TEST_PAGE_PATH),
                MANIFEST_TEST_PAGE_TITLE);
        addShortcutToTab(mTab, "", true, AppType.SHORTCUT);

        Assert.assertFalse(mShortcutHelperDelegate.mRequestedShortcutAdaptable);

        mShortcutHelperDelegate.clearRequestedShortcutData();

        // Test the adaptive icon.
        loadUrl(
                mTestServerRule.getServer().getURL(MASKABLE_MANIFEST_TEST_PAGE_PATH),
                MANIFEST_TEST_PAGE_TITLE);
        addShortcutToTab(mTab, "", true, AppType.SHORTCUT);

        Assert.assertTrue(mShortcutHelperDelegate.mRequestedShortcutAdaptable);
    }

    @Test
    @SmallTest
    @Feature("{Webapp}")
    public void testAddBookmarkShortcut() throws Exception {
        loadUrl(NORMAL_HTML, NORMAL_TITLE);
        addShortcutToTab(mTab, "", true, /* expectedDialogType= */ AppType.SHORTCUT);

        // Make sure the intent's parameters make sense.
        Assert.assertEquals(NORMAL_TITLE, mShortcutHelperDelegate.mRequestedShortcutTitle);

        Intent launchIntent = mShortcutHelperDelegate.mRequestedShortcutIntent;
        Assert.assertEquals(mActivity.getPackageName(), launchIntent.getPackage());
        Assert.assertEquals(Intent.ACTION_VIEW, launchIntent.getAction());
        Assert.assertEquals(NORMAL_HTML, launchIntent.getDataString());
    }

    @Test
    @SmallTest
    @Feature("{Webapp}")
    public void testAddWebappShortcutsWithoutTitleEdit() throws Exception {
        // Add a webapp shortcut using the page's title.
        loadUrl(WEBAPP_HTML, WEBAPP_TITLE);
        addShortcutToTab(mTab, "", true, /* expectedDialogType= */ AppType.SHORTCUT);
        Assert.assertEquals(WEBAPP_TITLE, mShortcutHelperDelegate.mRequestedShortcutTitle);
    }

    @Test
    @SmallTest
    @Feature("{Webapp}")
    public void testAddWebappShortcutsWithTitleEdit() throws Exception {
        // Add a webapp shortcut with a custom title.
        loadUrl(WEBAPP_HTML, WEBAPP_TITLE);
        addShortcutToTab(mTab, EDITED_WEBAPP_TITLE, true, AppType.SHORTCUT);
        Assert.assertEquals(EDITED_WEBAPP_TITLE, mShortcutHelperDelegate.mRequestedShortcutTitle);
    }

    @Test
    @SmallTest
    @Feature("{Webapp}")
    public void testAddWebappShortcutsWithApplicationName() throws Exception {
        loadUrl(META_APP_NAME_HTML, META_APP_NAME_PAGE_TITLE);
        addShortcutToTab(mTab, "", true, /* expectedDialogType= */ AppType.SHORTCUT);
        Assert.assertEquals(META_APP_NAME_TITLE, mShortcutHelperDelegate.mRequestedShortcutTitle);
    }

    @Test
    @SmallTest
    @Feature("{Webapp}")
    @Restriction(Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE)
    @CommandLineFlags.Add(ContentSwitches.DISABLE_POPUP_BLOCKING)
    public void testAddWebappShortcutWithEmptyPage() {
        Tab spawnedPopup = spawnPopupInBackground("");
        addShortcutToTab(spawnedPopup, "", /* expectAdded= */ true, AppType.SHORTCUT);
    }

    /** Tests that the appinstalled event is fired when an app is installed. */
    @Test
    @SmallTest
    @Feature("{Webapp}")
    public void testAddWebappShortcutAppInstalledEvent() throws Exception {
        loadUrl(
                WebappTestPage.getServiceWorkerUrlWithAction(
                        mTestServerRule.getServer(), "verify_appinstalled"),
                WebappTestPage.PAGE_TITLE);

        addShortcutToTab(mTab, "", true, AppType.SHORTCUT);

        // Wait for the tab title to change. This will happen (due to the JavaScript
        // that runs
        // in the page) once the appinstalled event has been fired twice: once to test
        // addEventListener('appinstalled'), once to test onappinstalled attribute.
        new TabTitleObserver(mTab, "Got appinstalled: listener, attr").waitForTitleUpdate(3);
    }

    private void loadUrl(String url, String expectedPageTitle) throws Exception {
        new TabLoadObserver(mTab, expectedPageTitle, null).fullyLoadUrl(url);
    }

    private void addShortcutToTab(
            Tab tab, String title, boolean expectAdded, @AppType int expectedDialogType) {
        // Add the shortcut.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    boolean started =
                            new TestAddToHomescreenCoordinator(
                                            tab.getWebContents(),
                                            mActivity,
                                            mActivity.getWindowAndroid(),
                                            mActivity.getModalDialogManager(),
                                            title,
                                            expectedDialogType)
                                    .showForAppMenu(
                                            AppMenuVerbiage.APP_MENU_OPTION_ADD_TO_HOMESCREEN);
                    Assert.assertEquals(expectAdded, started);
                });

        // Make sure that the shortcut was added.
        if (expectAdded) {
            CriteriaHelper.pollUiThread(
                    () -> {
                        Criteria.checkThat(
                                mShortcutHelperDelegate.mRequestedShortcutIntent,
                                Matchers.notNullValue());
                    });
        }
    }

    /** Spawns popup via window.open() at {@link url}. */
    private Tab spawnPopupInBackground(final String url) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTab.getWebContents()
                            .evaluateJavaScriptForTests(
                                    "(function() {" + "window.open('" + url + "');" + "})()", null);
                });

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            mActivityTestRule
                                    .getActivity()
                                    .getTabModelSelector()
                                    .getModel(false)
                                    .getCount(),
                            Matchers.is(2));
                });

        TabModel tabModel = mActivityTestRule.getActivity().getTabModelSelector().getModel(false);
        Assert.assertEquals(0, tabModel.indexOf(mTab));
        return mActivityTestRule.getActivity().getTabModelSelector().getModel(false).getTabAt(1);
    }
}