diff --git a/chrome/browser/supervised_user/BUILD.gn b/chrome/browser/supervised_user/BUILD.gn index c83ecf04a2154..fc106e0409495 100644 --- a/chrome/browser/supervised_user/BUILD.gn +++ b/chrome/browser/supervised_user/BUILD.gn @@ -104,6 +104,7 @@ if (is_android) { annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ] sources = [ + "android/java/src/org/chromium/chrome/browser/supervised_user/ParentAuthDelegateProvider.java", "android/java/src/org/chromium/chrome/browser/supervised_user/WebsiteParentApproval.java", "android/java/src/org/chromium/chrome/browser/supervised_user/website_approval/WebsiteApprovalCoordinator.java", "android/java/src/org/chromium/chrome/browser/supervised_user/website_approval/WebsiteApprovalMediator.java", @@ -120,12 +121,12 @@ if (is_android) { generate_jni("test_support_jni_headers") { testonly = true - sources = [ "android/java/src/org/chromium/chrome/browser/supervised_user/test/SupervisedUserSettingsBridge.java" ] + sources = [ "android/java/src/org/chromium/chrome/browser/supervised_user/SupervisedUserSettingsBridge.java" ] } android_library("test_support_java") { testonly = true - sources = [ "android/java/src/org/chromium/chrome/browser/supervised_user/test/SupervisedUserSettingsBridge.java" ] + sources = [ "android/java/src/org/chromium/chrome/browser/supervised_user/SupervisedUserSettingsBridge.java" ] deps = [ ":test_support_jni_headers", "//base:jni_java", @@ -140,23 +141,35 @@ if (is_android) { android_library("javatests") { testonly = true sources = [ - "android/java/src/org/chromium/chrome/browser/supervised_user/test/WebsiteParentApprovalTest.java", + "android/java/src/org/chromium/chrome/browser/supervised_user/WebsiteParentApprovalTest.java", "android/javatests/src/org/chromium/chrome/browser/supervised_user/WebsiteParentApprovalMetricsUnitTest.java", "android/javatests/src/org/chromium/chrome/browser/supervised_user/website_approval/WebsiteApprovalSheetContentUnitTest.java", ] deps = [ ":test_support_java", + "//base:base_java", "//base:base_java_test_support", "//chrome/android:chrome_java", "//chrome/browser/flags:java", "//chrome/browser/profiles/android:java", + "//chrome/browser/signin/services/android:java", + "//chrome/browser/supervised_user:parent_auth_delegate_java", "//chrome/browser/supervised_user:supervised_user_metrics_java", "//chrome/browser/supervised_user:website_parent_approval_java", "//chrome/test/android:chrome_java_integration_test_support", + "//chrome/test/android:chrome_java_test_pagecontroller", + "//components/browser_ui/bottomsheet/android:java", + "//components/browser_ui/bottomsheet/android/test:java", + "//components/signin/public/android:java", + "//components/signin/public/android:signin_java_test_support", "//content/public/test/android:content_java_test_support", "//net/android:net_java_test_support", + "//third_party/android_deps:espresso_java", "//third_party/androidx:androidx_test_runner_java", "//third_party/junit:junit", + "//third_party/mockito:mockito_java", + "//ui/android:ui_java_test_support", + "//ui/android:ui_no_recycler_view_java", "//url:gurl_java", ] } diff --git a/chrome/browser/supervised_user/android/java/res/layout/website_approval_bottom_sheet.xml b/chrome/browser/supervised_user/android/java/res/layout/website_approval_bottom_sheet.xml index 61834bd7d1cb9..f1a54c257de5c 100644 --- a/chrome/browser/supervised_user/android/java/res/layout/website_approval_bottom_sheet.xml +++ b/chrome/browser/supervised_user/android/java/res/layout/website_approval_bottom_sheet.xml @@ -11,6 +11,7 @@ found in the LICENSE file. doesn't support dynamic colors. --> { sTestingInstance = parentAuthDelegate; }); + } + + /** + * Returns singleton instance. + */ + @MainThread + public static ParentAuthDelegate getInstance() { + ThreadUtils.assertOnUiThread(); + if (sTestingInstance != null) { + return sTestingInstance; + } + if (sInstance == null) { + sInstance = new ParentAuthDelegateImpl(); + } + return sInstance; + } +} \ No newline at end of file diff --git a/chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/test/SupervisedUserSettingsBridge.java b/chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/SupervisedUserSettingsBridge.java similarity index 82% rename from chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/test/SupervisedUserSettingsBridge.java rename to chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/SupervisedUserSettingsBridge.java index ed2388362c4e3..0e10477337202 100644 --- a/chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/test/SupervisedUserSettingsBridge.java +++ b/chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/SupervisedUserSettingsBridge.java @@ -1,8 +1,8 @@ -// Copyright 2022 The Chromium Authors. All rights reserved. +// 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.chrome.browser.supervised_user.test; +package org.chromium.chrome.browser.supervised_user; import org.chromium.base.annotations.NativeMethods; import org.chromium.chrome.browser.profiles.Profile; @@ -13,7 +13,7 @@ * * This should only be used in tests. */ -public class SupervisedUserSettingsBridge { +class SupervisedUserSettingsBridge { /** Set the website filtering behaviour for this user. */ static void setFilteringBehavior(Profile profile, @FilteringBehavior int setting) { SupervisedUserSettingsBridgeJni.get().setFilteringBehavior(profile, setting); diff --git a/chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/WebsiteParentApproval.java b/chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/WebsiteParentApproval.java index e4c12780820f4..869255beca61d 100644 --- a/chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/WebsiteParentApproval.java +++ b/chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/WebsiteParentApproval.java @@ -86,7 +86,7 @@ private static boolean isLocalApprovalSupported() { @CalledByNative private static void requestLocalApproval(WindowAndroid windowAndroid, GURL url) { // First ask the parent to authenticate. - ParentAuthDelegate delegate = new ParentAuthDelegateImpl(); + ParentAuthDelegate delegate = ParentAuthDelegateProvider.getInstance(); FaviconHelper faviconHelper = new FaviconHelper(); delegate.requestLocalAuth(windowAndroid, url, (success) -> { onParentAuthComplete(success, windowAndroid, url, faviconHelper); }); diff --git a/chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/WebsiteParentApprovalTest.java b/chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/WebsiteParentApprovalTest.java new file mode 100644 index 0000000000000..67680b98931d3 --- /dev/null +++ b/chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/WebsiteParentApprovalTest.java @@ -0,0 +1,249 @@ +// 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.chrome.browser.supervised_user; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isRoot; +import static androidx.test.espresso.matcher.ViewMatchers.withId; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import androidx.test.filters.MediumTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.quality.Strictness; + +import org.chromium.base.Callback; +import org.chromium.base.test.util.CommandLineFlags; +import org.chromium.base.test.util.CriteriaHelper; +import org.chromium.base.test.util.DoNotBatch; +import org.chromium.base.test.util.JniMocker; +import org.chromium.chrome.browser.ChromeTabbedActivity; +import org.chromium.chrome.browser.flags.ChromeFeatureList; +import org.chromium.chrome.browser.flags.ChromeSwitches; +import org.chromium.chrome.browser.profiles.Profile; +import org.chromium.chrome.browser.superviseduser.FilteringBehavior; +import org.chromium.chrome.test.ChromeJUnit4ClassRunner; +import org.chromium.chrome.test.ChromeTabbedActivityTestRule; +import org.chromium.chrome.test.util.browser.Features.EnableFeatures; +import org.chromium.chrome.test.util.browser.signin.SigninTestRule; +import org.chromium.components.browser_ui.bottomsheet.BottomSheetController; +import org.chromium.components.browser_ui.bottomsheet.BottomSheetTestSupport; +import org.chromium.content_public.browser.WebContents; +import org.chromium.content_public.browser.test.util.DOMUtils; +import org.chromium.content_public.browser.test.util.TestThreadUtils; +import org.chromium.net.test.EmbeddedTestServer; +import org.chromium.ui.base.WindowAndroid; +import org.chromium.ui.test.util.DisableAnimationsTestRule; +import org.chromium.ui.test.util.ViewUtils; +import org.chromium.url.GURL; + +import java.util.concurrent.TimeoutException; + +/** + * Tests the local website approval flow. + */ +@RunWith(ChromeJUnit4ClassRunner.class) +@DoNotBatch(reason = "Running tests in parallel can interfere with each tests setup." + + "The code under tests involes setting static methods and features, " + + "which must remain unchanged for the duration of the test.") +@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE}) +@EnableFeatures( + {ChromeFeatureList.LOCAL_WEB_APPROVALS, ChromeFeatureList.WEB_FILTER_INTERSTITIAL_REFRESH}) +public class WebsiteParentApprovalTest { + // TODO(b/243916194): Expand the test coverage beyond the completion callback, up to the page + // refresh. + // (TODO b/243916194): Expand test until the metric collection step on the native side. Requires + // not mocking the natives so that the flow moves onto their real execution. + + public ChromeTabbedActivityTestRule mTabbedActivityTestRule = + new ChromeTabbedActivityTestRule(); + public SigninTestRule mSigninTestRule = new SigninTestRule(); + + // Destroy TabbedActivityTestRule before SigninTestRule to remove observers of + // FakeAccountManagerFacade. + @Rule + public final RuleChain mRuleChain = + RuleChain.outerRule(mSigninTestRule).around(mTabbedActivityTestRule); + @Rule + public final JniMocker mocker = new JniMocker(); + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS); + @Rule + public final DisableAnimationsTestRule sDisableAnimationsRule = new DisableAnimationsTestRule(); + + private static final String TEST_PAGE = "/chrome/test/data/android/about.html"; + private static final String LOCAL_APPROVALS_BUTTON_NODE_ID = "local-approvals-button"; + + private EmbeddedTestServer mTestServer; + private String mBlockedUrl; + private WebContents mWebContents; + private BottomSheetController mBottomSheetController; + private BottomSheetTestSupport mBottomSheetTestSupport; + + @Mock + private WebsiteParentApproval.Natives mWebsiteParentApprovalNativesMock; + @Mock + private ParentAuthDelegate mParentAuthDelegateMock; + + @Before + public void setUp() throws TimeoutException { + mTestServer = mTabbedActivityTestRule.getEmbeddedTestServerRule().getServer(); + mBlockedUrl = mTestServer.getURL(TEST_PAGE); + mTabbedActivityTestRule.startMainActivityOnBlankPage(); + TestThreadUtils.runOnUiThreadBlocking(() -> { + ChromeTabbedActivity activity = mTabbedActivityTestRule.getActivity(); + mBottomSheetController = + activity.getRootUiCoordinatorForTesting().getBottomSheetController(); + mBottomSheetTestSupport = new BottomSheetTestSupport(mBottomSheetController); + }); + + mSigninTestRule.addChildTestAccountThenWaitForSignin(); + TestThreadUtils.runOnUiThreadBlocking(() -> { + SupervisedUserSettingsBridge.setFilteringBehavior( + Profile.getLastUsedRegularProfile(), FilteringBehavior.BLOCK); + }); + mWebContents = mTabbedActivityTestRule.getWebContents(); + + mocker.mock(WebsiteParentApprovalJni.TEST_HOOKS, mWebsiteParentApprovalNativesMock); + doNothing() + .when(mWebsiteParentApprovalNativesMock) + .fetchFavicon(any(GURL.class), any(Integer.class), any(Integer.class), + any(Callback.class)); + //@TODO b:243916194 : Trigger the execution of the real (void) method. + doNothing().when(mWebsiteParentApprovalNativesMock).onCompletion(any(Integer.class)); + + // @TODO b:243916194 : Once we start consuming mParentAuthDelegateMock + // .isLocalAuthSupported we should add a mocked behaviour in this test. + ParentAuthDelegateProvider.setInstanceForTests(mParentAuthDelegateMock); + } + + private void mockParentAuthDelegateRequestLocalAuthResponse(boolean result) { + doAnswer(invocation -> { + Callback onCompletionCallback = invocation.getArgument(2); + onCompletionCallback.onResult(result); + return null; + }) + .when(mParentAuthDelegateMock) + .requestLocalAuth(any(WindowAndroid.class), any(GURL.class), any(Callback.class)); + } + + private void clickAskInPerson() { + try { + String contents = + DOMUtils.getNodeContents(mWebContents, LOCAL_APPROVALS_BUTTON_NODE_ID); + } catch (TimeoutException e) { + throw new RuntimeException("Local approval button not found"); + } + DOMUtils.clickNodeWithJavaScript(mWebContents, LOCAL_APPROVALS_BUTTON_NODE_ID); + } + + private void checkParentApprovalBottomSheetVisible() { + onView(isRoot()).check(ViewUtils.waitForView( + withId(R.id.local_parent_approval_layout), ViewUtils.VIEW_VISIBLE)); + // Ensure all animations have ended before allowing interaction with the view. + TestThreadUtils.runOnUiThreadBlocking( + () -> { mBottomSheetTestSupport.endAllAnimations(); }); + } + + private void clickApprove() { + checkParentApprovalBottomSheetVisible(); + onView(withId(R.id.approve_button)) + .check(matches(isCompletelyDisplayed())) + .perform(click()); + } + + private void clickDoNotApprove() { + checkParentApprovalBottomSheetVisible(); + onView(withId(R.id.deny_button)).check(matches(isCompletelyDisplayed())).perform(click()); + } + + private void checkParentApprovalScreenClosedAfterClick() { + // Ensure all animations have ended. Otherwise the following check may fail. + TestThreadUtils.runOnUiThreadBlocking( + () -> { mBottomSheetTestSupport.endAllAnimations(); }); + onView(isRoot()).check(ViewUtils.waitForView(withId(R.id.local_parent_approval_layout), + ViewUtils.VIEW_INVISIBLE | ViewUtils.VIEW_GONE | ViewUtils.VIEW_NULL)); + } + + @Test + @MediumTest + public void parentApprovesScreenVisibilityAfterApproval() { + mockParentAuthDelegateRequestLocalAuthResponse(true); + mTabbedActivityTestRule.loadUrl(mBlockedUrl); + + clickAskInPerson(); + clickApprove(); + + checkParentApprovalScreenClosedAfterClick(); + } + + @Test + @MediumTest + public void parentApprovesScreenVisibilityAfterRejection() { + mockParentAuthDelegateRequestLocalAuthResponse(true); + mTabbedActivityTestRule.loadUrl(mBlockedUrl); + + clickAskInPerson(); + clickDoNotApprove(); + + checkParentApprovalScreenClosedAfterClick(); + } + + @Test + @MediumTest + public void parentApprovesLocally() { + mockParentAuthDelegateRequestLocalAuthResponse(true); + mTabbedActivityTestRule.loadUrl(mBlockedUrl); + + clickAskInPerson(); + clickApprove(); + + verify(mWebsiteParentApprovalNativesMock, + timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL).times(1)) + .onCompletion(AndroidLocalWebApprovalFlowOutcome.APPROVED); + } + + @Test + @MediumTest + public void parentRejectsLocally() { + mockParentAuthDelegateRequestLocalAuthResponse(true); + mTabbedActivityTestRule.loadUrl(mBlockedUrl); + + clickAskInPerson(); + clickDoNotApprove(); + + verify(mWebsiteParentApprovalNativesMock, + timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL).times(1)) + .onCompletion(AndroidLocalWebApprovalFlowOutcome.REJECTED); + } + + @Test + @MediumTest + public void parentAuthorizationFailure() { + mockParentAuthDelegateRequestLocalAuthResponse(false); + mTabbedActivityTestRule.loadUrl(mBlockedUrl); + + clickAskInPerson(); + + verify(mWebsiteParentApprovalNativesMock, + timeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL).times(1)) + .onCompletion(AndroidLocalWebApprovalFlowOutcome.INCOMPLETE); + } +} diff --git a/chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/test/WebsiteParentApprovalTest.java b/chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/test/WebsiteParentApprovalTest.java deleted file mode 100644 index d64fc4247c74e..0000000000000 --- a/chrome/browser/supervised_user/android/java/src/org/chromium/chrome/browser/supervised_user/test/WebsiteParentApprovalTest.java +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2022 The Chromium Authors. All rights reserved. -// 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.supervised_user.test; - -import androidx.test.filters.MediumTest; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.chromium.base.test.util.Batch; -import org.chromium.base.test.util.CommandLineFlags; -import org.chromium.chrome.browser.flags.ChromeFeatureList; -import org.chromium.chrome.browser.flags.ChromeSwitches; -import org.chromium.chrome.browser.profiles.Profile; -import org.chromium.chrome.browser.superviseduser.FilteringBehavior; -import org.chromium.chrome.test.ChromeJUnit4ClassRunner; -import org.chromium.chrome.test.ChromeTabbedActivityTestRule; -import org.chromium.chrome.test.util.browser.Features.EnableFeatures; -import org.chromium.content_public.browser.test.util.TestThreadUtils; -import org.chromium.net.test.EmbeddedTestServer; - -import java.util.concurrent.TimeoutException; - -/** - * Tests the local website approval flow. - */ -@RunWith(ChromeJUnit4ClassRunner.class) -@Batch(Batch.PER_CLASS) -@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE}) -@EnableFeatures( - {ChromeFeatureList.LOCAL_WEB_APPROVALS, ChromeFeatureList.WEB_FILTER_INTERSTITIAL_REFRESH}) -public class WebsiteParentApprovalTest { - @Rule - public ChromeTabbedActivityTestRule mTabbedActivityTestRule = - new ChromeTabbedActivityTestRule(); - - private static final String TEST_PAGE = "/chrome/test/data/android/about.html"; - - private EmbeddedTestServer mTestServer; - private String mTestPage; - - @Before - public void setUp() throws TimeoutException { - mTestServer = mTabbedActivityTestRule.getEmbeddedTestServerRule().getServer(); - mTestPage = mTestServer.getURL(TEST_PAGE); - - mTabbedActivityTestRule.startMainActivityWithURL(mTestPage); - - // Set up website filtering configuration. - configureAllowOnlyCertainSites(); - } - - @Test - @MediumTest - public void parentApprovesLocally() { - // Navigate to a blocked website. - loadUrl(mTestPage); - - // Verify the interstitial screen is shown and click ask in person. - clickAskInPerson(); - - // Mock successful parent auth. - - clickAllow(); - - checkTestPageLoaded(); - } - - // TODO(crbug.com/1340913): add test cases to cover rejection, failures, etc. - - private void configureAllowOnlyCertainSites() { - // Set behaviour to BLOCK. - TestThreadUtils.runOnUiThreadBlocking(() -> { - SupervisedUserSettingsBridge.setFilteringBehavior( - Profile.getLastUsedRegularProfile(), FilteringBehavior.BLOCK); - }); - } - - /** - * Loads a URL and checks that it loaded successfully. - * - * This does not guarantee that the actual content was displayed, as opposed to the blocked - * website interstitial. - */ - private void loadUrl(String url) { - mTabbedActivityTestRule.loadUrl(mTestPage); - // TODO: check the load was successful. - } - - private void clickAskInPerson() { - // See eg. - // https://source.chromium.org/chromium/chromium/src/+/main:content/public/test/android/javatests/src/org/chromium/content_public/browser/test/util/JavaScriptUtils.java;l=117;drc=cfd951a304bb7a1e6424c614c094f5bc581ca8b7 - // - // C++ tests: - // https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/supervised_user/supervised_user_url_filter_browsertest.cc;l=80;drc=cfd951a304bb7a1e6424c614c094f5bc581ca8b7 - } - - private void clickAllow() { - // TODO(crbug.com/1340913): implement - // Parent clicks approve. - // Use UiAutomator? - } - - private void clickDontAllow() { - // TODO(crbug.com/1340913): implement - // Parent clicks don't allow. - } - - private void checkTestPageLoaded() { - // TODO(crbug.com/1340913): implement a simple check on the presence of expected text in the - // page. - } -}