chromium/android_webview/javatests/src/org/chromium/android_webview/test/AwFileChooserTest.java

// Copyright 2022 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.android_webview.test;

import android.content.Intent;
import android.net.Uri;
import android.webkit.WebChromeClient;

import androidx.test.filters.SmallTest;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.UseParametersRunnerFactory;

import org.chromium.android_webview.AwContents;
import org.chromium.android_webview.AwContentsClient.FileChooserParamsImpl;
import org.chromium.android_webview.AwSettings;
import org.chromium.android_webview.test.util.CommonResources;
import org.chromium.android_webview.test.util.JSUtils;
import org.chromium.base.FileUtils;
import org.chromium.base.PathUtils;
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.net.test.util.TestWebServer;

import java.io.File;
import java.util.concurrent.TimeoutException;

/** Integration tests for the WebChromeClient.onShowFileChooser method. */
@RunWith(Parameterized.class)
@UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class)
@DoNotBatch(reason = "Shared dependencies among the tests cause conflicts during batch testing.")
public class AwFileChooserTest extends AwParameterizedTest {
    @Rule public AwActivityTestRule mActivityTestRule;
    private AwTestContainerView mTestContainerView;

    private TestAwContentsClient mContentsClient = new TestAwContentsClient();
    private AwContents mAwContents;
    private AwSettings mAwSettings;
    private TestWebServer mWebServer;
    private TestAwContentsClient.ShowFileChooserHelper mShowFileChooserHelper;

    private static final String INDEX_HTML_ROUTE = "/index.html";
    private static final String FILE_CHOICE_BUTTON_ID = "fileChooserInput";
    private File mTestFile1;
    private File mTestFile2;
    private File mTestDirectory;
    private static final String TEST_DIRECTORY_PATH = PathUtils.getDataDirectory() + "/test";
    private static final String EMPTY_STRING = "";

    private static final String TAG = "AwFileChooserTest";

    public AwFileChooserTest(AwSettingsMutation param) {
        this.mActivityTestRule = new AwActivityTestRule(param.getMutation());
    }

    @Before
    public void setUp() throws Exception {
        mContentsClient = new TestAwContentsClient();
        mTestContainerView = mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient);
        mAwContents = mTestContainerView.getAwContents();
        mShowFileChooserHelper = mContentsClient.getShowFileChooserHelper();
        mAwSettings = mActivityTestRule.getAwSettingsOnUiThread(mAwContents);
        mAwSettings.setAllowFileAccess(true);
        AwActivityTestRule.enableJavaScriptOnUiThread(mAwContents);

        mTestDirectory = new File(TEST_DIRECTORY_PATH);
        Assert.assertTrue(mTestDirectory.mkdirs());
        mTestFile1 = new File(mTestDirectory.getPath() + "/test1.txt");
        Assert.assertTrue(mTestFile1.createNewFile());
        mTestFile2 = new File(mTestDirectory.getPath() + "/test2.txt");
        Assert.assertTrue(mTestFile2.createNewFile());

        Assert.assertTrue(
                "Test file 1 is an empty string!",
                !EMPTY_STRING.equalsIgnoreCase(mTestFile1.getPath()));
        Assert.assertNotNull("Test File 1 is null!", mTestFile1.getPath());
        Assert.assertTrue(
                "Test file 2 is an empty string!",
                !EMPTY_STRING.equalsIgnoreCase(mTestFile2.getPath()));
        Assert.assertNotNull("Test File 2 is null!", mTestFile2.getPath());

        mWebServer = TestWebServer.start();
    }

    @After
    public void tearDown() {
        if (mWebServer != null) mWebServer.shutdown();
        Assert.assertTrue(FileUtils.recursivelyDeleteFile(mTestDirectory, FileUtils.DELETE_ALL));
    }

    @Test
    @SmallTest
    public void testShowSingleFileChoice() throws Throwable {
        final String singleFileUploadPageHtml =
                CommonResources.makeHtmlPageFrom(
                        /* headers= */ "",
                        /* body= */ "<input type='file' accept='.txt' id='"
                                + FILE_CHOICE_BUTTON_ID
                                + "' /><br><br>");
        final String url = mWebServer.setResponse(INDEX_HTML_ROUTE, singleFileUploadPageHtml, null);

        mShowFileChooserHelper.setChosenFilesToUpload(
                new String[] {Uri.fromFile(mTestFile1).toString()});
        mActivityTestRule.loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), url);

        clickSelectFileButtonAndWaitForCallback("1");
        final FileChooserParamsImpl params = mShowFileChooserHelper.getFileParams();
        Assert.assertEquals(WebChromeClient.FileChooserParams.MODE_OPEN, params.getMode());
    }

    @Test
    @SmallTest
    public void testShowMultipleFileChoice() throws Throwable {
        final String multipleFileUploadPageHtml =
                CommonResources.makeHtmlPageFrom(
                        /* headers= */ "",
                        /* body= */ "<input type='file' accept='.txt' id='"
                                + FILE_CHOICE_BUTTON_ID
                                + "' multiple/><br><br>");
        final String url =
                mWebServer.setResponse(INDEX_HTML_ROUTE, multipleFileUploadPageHtml, null);

        mShowFileChooserHelper.setChosenFilesToUpload(
                new String[] {
                    Uri.fromFile(mTestFile1).toString(), Uri.fromFile(mTestFile2).toString()
                });
        mActivityTestRule.loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), url);

        clickSelectFileButtonAndWaitForCallback("2");
        final FileChooserParamsImpl params = mShowFileChooserHelper.getFileParams();
        Assert.assertEquals(WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE, params.getMode());
    }

    @Test
    @SmallTest
    public void testShowDirectoryChoice() throws Throwable {
        final String multipleFileUploadPageHtml =
                CommonResources.makeHtmlPageFrom(
                        /* headers= */ "",
                        /* body= */ "<input type='file' accept='.txt' id='"
                                + FILE_CHOICE_BUTTON_ID
                                + "' webkitdirectory ><br><br>");
        final String url =
                mWebServer.setResponse(INDEX_HTML_ROUTE, multipleFileUploadPageHtml, null);

        mShowFileChooserHelper.setChosenFilesToUpload(
                new String[] {
                    Uri.fromFile(mTestFile1).toString(), Uri.fromFile(mTestFile2).toString()
                });
        mActivityTestRule.loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), url);

        clickSelectFileButtonAndWaitForCallback("2");
        final FileChooserParamsImpl params = mShowFileChooserHelper.getFileParams();
        // Upload folder is not currently supported in webview
        // As a workaround, it is treated as OPEN_MULTIPLE_MODE
        Assert.assertEquals(WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE, params.getMode());
    }

    @Test
    @SmallTest
    public void testAcceptTypes() throws Throwable {
        final String[] expectedIntentExtraTypes = {"application/pdf", "text/plain", "image/png"};
        final String expectedIntentType = expectedIntentExtraTypes[0];
        final String expectedAcceptTypesString = ".pdf,.txt,.png";
        final String singleFileUploadPageHtml =
                CommonResources.makeHtmlPageFrom(
                        /* headers= */ "",
                        /* body= */ "<input type='file' accept='"
                                + expectedAcceptTypesString
                                + "' id='"
                                + FILE_CHOICE_BUTTON_ID
                                + "' /><br><br>");
        final String url = mWebServer.setResponse(INDEX_HTML_ROUTE, singleFileUploadPageHtml, null);

        mShowFileChooserHelper.setChosenFilesToUpload(
                new String[] {Uri.fromFile(mTestFile1).toString()});
        mActivityTestRule.loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), url);

        clickSelectFileButtonAndWaitForCallback("1");
        final FileChooserParamsImpl params = mShowFileChooserHelper.getFileParams();
        Assert.assertEquals(expectedAcceptTypesString, params.getAcceptTypesString());

        // Testing FileChooserParamsImpl.createIntent API
        // Verifies that the file choice type and the extra types are set properly
        Intent i = params.createIntent();
        Assert.assertEquals(
                i.getStringArrayExtra(Intent.EXTRA_MIME_TYPES), expectedIntentExtraTypes);
        Assert.assertEquals(i.getType(), expectedIntentType);
    }

    @Test
    @SmallTest
    public void testIsCaptureEnabled() throws Throwable {
        final boolean captureEnabled = true;
        final String singleFileUploadPageHtml =
                CommonResources.makeHtmlPageFrom(
                        /* headers= */ "",
                        /* body= */ "<input type='file' accept='.txt' id='"
                                + FILE_CHOICE_BUTTON_ID
                                + "' capture/><br><br>");
        final String url = mWebServer.setResponse(INDEX_HTML_ROUTE, singleFileUploadPageHtml, null);

        mShowFileChooserHelper.setChosenFilesToUpload(
                new String[] {Uri.fromFile(mTestFile1).toString()});
        mActivityTestRule.loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), url);

        clickSelectFileButtonAndWaitForCallback("1");
        final FileChooserParamsImpl params = mShowFileChooserHelper.getFileParams();
        Assert.assertEquals(captureEnabled, params.isCaptureEnabled());
    }

    @Test
    @SmallTest
    public void testIsCaptureDisabled() throws Throwable {
        final boolean captureEnabled = false;
        final String singleFileUploadPageHtml =
                CommonResources.makeHtmlPageFrom(
                        /* headers= */ "",
                        /* body= */ "<input type='file' accept='.txt' id='"
                                + FILE_CHOICE_BUTTON_ID
                                + "' /><br><br>");
        final String url = mWebServer.setResponse(INDEX_HTML_ROUTE, singleFileUploadPageHtml, null);

        mShowFileChooserHelper.setChosenFilesToUpload(
                new String[] {Uri.fromFile(mTestFile1).toString()});
        mActivityTestRule.loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), url);

        clickSelectFileButtonAndWaitForCallback("1");
        final FileChooserParamsImpl params = mShowFileChooserHelper.getFileParams();
        Assert.assertEquals(captureEnabled, params.isCaptureEnabled());
    }

    @Test
    @SmallTest
    public void testInvalidUriIsCanceled() throws Throwable {
        final String singleFileUploadPageHtml =
                CommonResources.makeHtmlPageFrom(
                        /* headers= */ "",
                        /* body= */ "<input type='file' accept='.txt' id='"
                                + FILE_CHOICE_BUTTON_ID
                                + "' /><br><br>");
        final String url = mWebServer.setResponse(INDEX_HTML_ROUTE, singleFileUploadPageHtml, null);

        mShowFileChooserHelper.setChosenFilesToUpload(
                // Using one real Uri and one invalid Uri because
                // ActivityTestRule#executeJavaScriptAndWaitForResult
                // waits for a DOM change to occur In order for the DOM to change, we must provide
                // at least one real Uri
                new String[] {Uri.fromFile(mTestFile1).toString(), "/BadUri/ThatIsnt/Valid"});
        mActivityTestRule.loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), url);
        clickSelectFileButtonAndWaitForCallback("1");

        // Using two real Uris and one invalid Uri
        mShowFileChooserHelper.setChosenFilesToUpload(
                new String[] {
                    Uri.fromFile(mTestFile1).toString(),
                    Uri.fromFile(mTestFile2).toString(),
                    "/BadUri/ThatIsnt/Valid"
                });
        clickSelectFileButtonAndWaitForCallback("2");
    }

    /** Simulates user clicking Choose File button. */
    private void clickSelectFileButton() throws Exception {
        JSUtils.clickNodeWithUserGesture(mAwContents.getWebContents(), FILE_CHOICE_BUTTON_ID);
    }

    /**
     *  Wait until the expected string matches what
     *  value is in the DOM returned  by the JavaScript code
     */
    private void pollJavascriptResult(String script, String expectedResult) {
        AwActivityTestRule.pollInstrumentationThread(
                () -> {
                    try {
                        return expectedResult.equals(executeJavaScriptAndWaitForResult(script));
                    } catch (Throwable t) {
                        throw new RuntimeException(t);
                    }
                });
    }

    private String executeJavaScriptAndWaitForResult(String code) throws Throwable {
        return mActivityTestRule.executeJavaScriptAndWaitForResult(
                mTestContainerView.getAwContents(), mContentsClient, code);
    }

    private void clickSelectFileButtonAndWaitForCallback(String expectedNumberOfFiles)
            throws TimeoutException, Exception, Throwable {
        int callCount = mShowFileChooserHelper.getCallCount();
        clickSelectFileButton();
        mShowFileChooserHelper.waitForCallback(callCount);

        final String pollFileObjectOnDom =
                "document.getElementById('" + FILE_CHOICE_BUTTON_ID + "').files.length";
        pollJavascriptResult(pollFileObjectOnDom, expectedNumberOfFiles);
    }
}