From 989de76169699beee2c92a8ce41bc636896a66b6 Mon Sep 17 00:00:00 2001 From: Joshua Trask Date: Wed, 4 Oct 2023 15:31:47 +0000 Subject: `v2` boilerplate for ongoing empty-state work. Just cutting over to our new experiment infrastructure before I continue porting over the changes that were originally prototyped in ag/24516421. Bug: 302311217 Test: IntentResolverUnitTests Change-Id: Ied1843bee2be6ffb42ba4f539f6168a9d07a77d9 --- .../v2/MultiProfilePagerAdapterTest.kt | 282 +++++++++++++++++++++ .../v2/emptystate/EmptyStateUiHelperTest.kt | 112 ++++++++ 2 files changed, 394 insertions(+) create mode 100644 java/tests/src/com/android/intentresolver/v2/MultiProfilePagerAdapterTest.kt create mode 100644 java/tests/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelperTest.kt (limited to 'java/tests') diff --git a/java/tests/src/com/android/intentresolver/v2/MultiProfilePagerAdapterTest.kt b/java/tests/src/com/android/intentresolver/v2/MultiProfilePagerAdapterTest.kt new file mode 100644 index 00000000..f1af9790 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/v2/MultiProfilePagerAdapterTest.kt @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2 + +import android.os.UserHandle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ListView +import androidx.test.platform.app.InstrumentationRegistry +import com.android.intentresolver.MultiProfilePagerAdapter.PROFILE_PERSONAL +import com.android.intentresolver.MultiProfilePagerAdapter.PROFILE_WORK +import com.android.intentresolver.R +import com.android.intentresolver.ResolverListAdapter +import com.android.intentresolver.any +import com.android.intentresolver.emptystate.EmptyStateProvider +import com.android.intentresolver.mock +import com.android.intentresolver.whenever +import com.google.common.collect.ImmutableList +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import java.util.function.Supplier +import org.junit.Test +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +class MultiProfilePagerAdapterTest { + private val PERSONAL_USER_HANDLE = UserHandle.of(10) + private val WORK_USER_HANDLE = UserHandle.of(20) + + private val context = InstrumentationRegistry.getInstrumentation().getContext() + private val inflater = Supplier { + LayoutInflater.from(context).inflate(R.layout.resolver_list_per_profile, null, false) + as ViewGroup + } + + @Test + fun testSinglePageProfileAdapter() { + val personalListAdapter = + mock { whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE) } + val pagerAdapter = + MultiProfilePagerAdapter( + { listAdapter: ResolverListAdapter -> listAdapter }, + { listView: ListView, bindAdapter: ResolverListAdapter -> + listView.setAdapter(bindAdapter) + }, + ImmutableList.of(personalListAdapter), + object : EmptyStateProvider {}, + { false }, + PROFILE_PERSONAL, + null, + null, + inflater, + { Optional.empty() } + ) + assertThat(pagerAdapter.count).isEqualTo(1) + assertThat(pagerAdapter.currentPage).isEqualTo(PROFILE_PERSONAL) + assertThat(pagerAdapter.currentUserHandle).isEqualTo(PERSONAL_USER_HANDLE) + assertThat(pagerAdapter.getAdapterForIndex(0)).isSameInstanceAs(personalListAdapter) + assertThat(pagerAdapter.activeListAdapter).isSameInstanceAs(personalListAdapter) + assertThat(pagerAdapter.inactiveListAdapter).isNull() + assertThat(pagerAdapter.personalListAdapter).isSameInstanceAs(personalListAdapter) + assertThat(pagerAdapter.workListAdapter).isNull() + assertThat(pagerAdapter.itemCount).isEqualTo(1) + // TODO: consider covering some of the package-private methods (and making them public?). + // TODO: consider exercising responsibilities as an implementation of a ViewPager adapter. + } + + @Test + fun testTwoProfilePagerAdapter() { + val personalListAdapter = + mock { whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE) } + val workListAdapter = + mock { whenever(getUserHandle()).thenReturn(WORK_USER_HANDLE) } + val pagerAdapter = + MultiProfilePagerAdapter( + { listAdapter: ResolverListAdapter -> listAdapter }, + { listView: ListView, bindAdapter: ResolverListAdapter -> + listView.setAdapter(bindAdapter) + }, + ImmutableList.of(personalListAdapter, workListAdapter), + object : EmptyStateProvider {}, + { false }, + PROFILE_PERSONAL, + WORK_USER_HANDLE, // TODO: why does this test pass even if this is null? + null, + inflater, + { Optional.empty() } + ) + assertThat(pagerAdapter.count).isEqualTo(2) + assertThat(pagerAdapter.currentPage).isEqualTo(PROFILE_PERSONAL) + assertThat(pagerAdapter.currentUserHandle).isEqualTo(PERSONAL_USER_HANDLE) + assertThat(pagerAdapter.getAdapterForIndex(0)).isSameInstanceAs(personalListAdapter) + assertThat(pagerAdapter.getAdapterForIndex(1)).isSameInstanceAs(workListAdapter) + assertThat(pagerAdapter.activeListAdapter).isSameInstanceAs(personalListAdapter) + assertThat(pagerAdapter.inactiveListAdapter).isSameInstanceAs(workListAdapter) + assertThat(pagerAdapter.personalListAdapter).isSameInstanceAs(personalListAdapter) + assertThat(pagerAdapter.workListAdapter).isSameInstanceAs(workListAdapter) + assertThat(pagerAdapter.itemCount).isEqualTo(2) + // TODO: consider covering some of the package-private methods (and making them public?). + // TODO: consider exercising responsibilities as an implementation of a ViewPager adapter; + // especially matching profiles to ListViews? + // TODO: test ProfileSelectedListener (and getters for "current" state) as the selected + // page changes. Currently there's no API to change the selected page directly; that's + // only possible through manipulation of the bound ViewPager. + } + + @Test + fun testTwoProfilePagerAdapter_workIsDefault() { + val personalListAdapter = + mock { whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE) } + val workListAdapter = + mock { whenever(getUserHandle()).thenReturn(WORK_USER_HANDLE) } + val pagerAdapter = + MultiProfilePagerAdapter( + { listAdapter: ResolverListAdapter -> listAdapter }, + { listView: ListView, bindAdapter: ResolverListAdapter -> + listView.setAdapter(bindAdapter) + }, + ImmutableList.of(personalListAdapter, workListAdapter), + object : EmptyStateProvider {}, + { false }, + PROFILE_WORK, // <-- This test specifically requests we start on work profile. + WORK_USER_HANDLE, // TODO: why does this test pass even if this is null? + null, + inflater, + { Optional.empty() } + ) + assertThat(pagerAdapter.count).isEqualTo(2) + assertThat(pagerAdapter.currentPage).isEqualTo(PROFILE_WORK) + assertThat(pagerAdapter.currentUserHandle).isEqualTo(WORK_USER_HANDLE) + assertThat(pagerAdapter.getAdapterForIndex(0)).isSameInstanceAs(personalListAdapter) + assertThat(pagerAdapter.getAdapterForIndex(1)).isSameInstanceAs(workListAdapter) + assertThat(pagerAdapter.activeListAdapter).isSameInstanceAs(workListAdapter) + assertThat(pagerAdapter.inactiveListAdapter).isSameInstanceAs(personalListAdapter) + assertThat(pagerAdapter.personalListAdapter).isSameInstanceAs(personalListAdapter) + assertThat(pagerAdapter.workListAdapter).isSameInstanceAs(workListAdapter) + assertThat(pagerAdapter.itemCount).isEqualTo(2) + // TODO: consider covering some of the package-private methods (and making them public?). + // TODO: test ProfileSelectedListener (and getters for "current" state) as the selected + // page changes. Currently there's no API to change the selected page directly; that's + // only possible through manipulation of the bound ViewPager. + } + + @Test + fun testBottomPaddingDelegate_default() { + val container = + mock { + whenever(getPaddingLeft()).thenReturn(1) + whenever(getPaddingTop()).thenReturn(2) + whenever(getPaddingRight()).thenReturn(3) + whenever(getPaddingBottom()).thenReturn(4) + } + val pagerAdapter = + MultiProfilePagerAdapter( + { listAdapter: ResolverListAdapter -> listAdapter }, + { listView: ListView, bindAdapter: ResolverListAdapter -> + listView.setAdapter(bindAdapter) + }, + ImmutableList.of(), + object : EmptyStateProvider {}, + { false }, + PROFILE_PERSONAL, + null, + null, + inflater, + { Optional.empty() } + ) + pagerAdapter.setupContainerPadding(container) + verify(container, never()).setPadding(any(), any(), any(), any()) + } + + @Test + fun testBottomPaddingDelegate_override() { + val container = + mock { + whenever(getPaddingLeft()).thenReturn(1) + whenever(getPaddingTop()).thenReturn(2) + whenever(getPaddingRight()).thenReturn(3) + whenever(getPaddingBottom()).thenReturn(4) + } + val pagerAdapter = + MultiProfilePagerAdapter( + { listAdapter: ResolverListAdapter -> listAdapter }, + { listView: ListView, bindAdapter: ResolverListAdapter -> + listView.setAdapter(bindAdapter) + }, + ImmutableList.of(), + object : EmptyStateProvider {}, + { false }, + PROFILE_PERSONAL, + null, + null, + inflater, + { Optional.of(42) } + ) + pagerAdapter.setupContainerPadding(container) + verify(container).setPadding(1, 2, 3, 42) + } + + @Test + fun testPresumedQuietModeEmptyStateForWorkProfile_whenQuiet() { + // TODO: this is "presumed" because the conditions to determine whether we "should" show an + // empty state aren't enforced to align with the conditions when we actually *would* -- I + // believe `shouldShowEmptyStateScreen` should be implemented in terms of the provider? + val personalListAdapter = + mock { + whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE) + whenever(getUnfilteredCount()).thenReturn(1) + } + val workListAdapter = + mock { + whenever(getUserHandle()).thenReturn(WORK_USER_HANDLE) + whenever(getUnfilteredCount()).thenReturn(1) + } + val pagerAdapter = + MultiProfilePagerAdapter( + { listAdapter: ResolverListAdapter -> listAdapter }, + { listView: ListView, bindAdapter: ResolverListAdapter -> + listView.setAdapter(bindAdapter) + }, + ImmutableList.of(personalListAdapter, workListAdapter), + object : EmptyStateProvider {}, + { true }, // <-- Work mode is quiet. + PROFILE_WORK, + WORK_USER_HANDLE, + null, + inflater, + { Optional.empty() } + ) + assertThat(pagerAdapter.shouldShowEmptyStateScreen(workListAdapter)).isTrue() + assertThat(pagerAdapter.shouldShowEmptyStateScreen(personalListAdapter)).isFalse() + } + + @Test + fun testPresumedQuietModeEmptyStateForWorkProfile_notWhenNotQuiet() { + // TODO: this is "presumed" because the conditions to determine whether we "should" show an + // empty state aren't enforced to align with the conditions when we actually *would* -- I + // believe `shouldShowEmptyStateScreen` should be implemented in terms of the provider? + val personalListAdapter = + mock { + whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE) + whenever(getUnfilteredCount()).thenReturn(1) + } + val workListAdapter = + mock { + whenever(getUserHandle()).thenReturn(WORK_USER_HANDLE) + whenever(getUnfilteredCount()).thenReturn(1) + } + val pagerAdapter = + MultiProfilePagerAdapter( + { listAdapter: ResolverListAdapter -> listAdapter }, + { listView: ListView, bindAdapter: ResolverListAdapter -> + listView.setAdapter(bindAdapter) + }, + ImmutableList.of(personalListAdapter, workListAdapter), + object : EmptyStateProvider {}, + { false }, // <-- Work mode is not quiet. + PROFILE_WORK, + WORK_USER_HANDLE, + null, + inflater, + { Optional.empty() } + ) + assertThat(pagerAdapter.shouldShowEmptyStateScreen(workListAdapter)).isFalse() + assertThat(pagerAdapter.shouldShowEmptyStateScreen(personalListAdapter)).isFalse() + } +} diff --git a/java/tests/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelperTest.kt b/java/tests/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelperTest.kt new file mode 100644 index 00000000..12943cd7 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelperTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.emptystate + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test + +class EmptyStateUiHelperTest { + private val context = InstrumentationRegistry.getInstrumentation().getContext() + + lateinit var rootContainer: ViewGroup + lateinit var emptyStateTitleView: View + lateinit var emptyStateSubtitleView: View + lateinit var emptyStateButtonView: View + lateinit var emptyStateProgressView: View + lateinit var emptyStateDefaultTextView: View + lateinit var emptyStateContainerView: View + lateinit var emptyStateRootView: View + lateinit var emptyStateUiHelper: EmptyStateUiHelper + + @Before + fun setup() { + rootContainer = FrameLayout(context) + LayoutInflater.from(context) + .inflate( + com.android.intentresolver.R.layout.resolver_list_per_profile, + rootContainer, + true + ) + emptyStateRootView = + rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state) + emptyStateTitleView = + rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state_title) + emptyStateSubtitleView = + rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state_subtitle) + emptyStateButtonView = + rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state_button) + emptyStateProgressView = + rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state_progress) + emptyStateDefaultTextView = rootContainer.requireViewById(com.android.internal.R.id.empty) + emptyStateContainerView = + rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state_container) + emptyStateUiHelper = EmptyStateUiHelper(rootContainer) + } + + @Test + fun testResetViewVisibilities() { + // First set each view's visibility to differ from the expected "reset" state so we can then + // assert that they're all reset afterward. + // TODO: for historic reasons "reset" doesn't cover `emptyStateContainerView`; should it? + emptyStateRootView.visibility = View.GONE + emptyStateTitleView.visibility = View.GONE + emptyStateSubtitleView.visibility = View.GONE + emptyStateButtonView.visibility = View.VISIBLE + emptyStateProgressView.visibility = View.VISIBLE + emptyStateDefaultTextView.visibility = View.VISIBLE + + emptyStateUiHelper.resetViewVisibilities() + + assertThat(emptyStateRootView.visibility).isEqualTo(View.VISIBLE) + assertThat(emptyStateTitleView.visibility).isEqualTo(View.VISIBLE) + assertThat(emptyStateSubtitleView.visibility).isEqualTo(View.VISIBLE) + assertThat(emptyStateButtonView.visibility).isEqualTo(View.INVISIBLE) + assertThat(emptyStateProgressView.visibility).isEqualTo(View.GONE) + assertThat(emptyStateDefaultTextView.visibility).isEqualTo(View.GONE) + } + + @Test + fun testShowSpinner() { + emptyStateTitleView.visibility = View.VISIBLE + emptyStateButtonView.visibility = View.VISIBLE + emptyStateProgressView.visibility = View.GONE + emptyStateDefaultTextView.visibility = View.VISIBLE + + emptyStateUiHelper.showSpinner() + + // TODO: should this cover any other views? Subtitle? + assertThat(emptyStateTitleView.visibility).isEqualTo(View.INVISIBLE) + assertThat(emptyStateButtonView.visibility).isEqualTo(View.INVISIBLE) + assertThat(emptyStateProgressView.visibility).isEqualTo(View.VISIBLE) + assertThat(emptyStateDefaultTextView.visibility).isEqualTo(View.GONE) + } + + @Test + fun testHide() { + emptyStateRootView.visibility = View.VISIBLE + + emptyStateUiHelper.hide() + + assertThat(emptyStateRootView.visibility).isEqualTo(View.GONE) + } +} -- cgit v1.2.3-59-g8ed1b