diff options
author | 2024-08-01 14:56:31 -0700 | |
---|---|---|
committer | 2024-08-15 11:41:15 -0700 | |
commit | 754d59681277b2afdd52b79746a593488f132e4b (patch) | |
tree | 73c52de4857c9f943bea1cf7524a422aa02fb1d6 | |
parent | 2f6086166956949bc5ab732795f671b1800cb613 (diff) |
Allow toggling of final shareousel item
When no items selected the UI will:
* change the chooser headline;
* make the custom action row invisible;
* make all targets disabled and all target icons greyscale.
Fix: 349468879
Test: atest IntentResolver-tests-unit
Test: manual functionality testing
Flag: com.android.intentresolver.unselect_final_item
Change-Id: I53b9c908943b1f1003cb0131a6dec8abc26ec782
14 files changed, 169 insertions, 43 deletions
diff --git a/aconfig/FeatureFlags.aconfig b/aconfig/FeatureFlags.aconfig index d6116c9a..6f7e75d5 100644 --- a/aconfig/FeatureFlags.aconfig +++ b/aconfig/FeatureFlags.aconfig @@ -109,3 +109,10 @@ flag { description: "Allow Shareousel selection change callback to update Intent#EXTRA_EXCLUDE_COMPONENTS" bug: "352496527" } + +flag { + name: "unselect_final_item" + namespace: "intentresolver" + description: "Allow toggling of final Shareousel item" + bug: "349468879" +} diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index c026ee59..4f77d248 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -162,6 +162,9 @@ } </string> + <!-- Title atop a sharing UI indicating that a selection needs to be made for sharing --> + <string name="select_items_to_share">Select items to share</string> + <!-- Title atop a sharing UI indicating that some number of images are being shared along with text [CHAR_LIMIT=50] --> <string name="sharing_images_with_text">{count, plural, diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 51d8785f..8871ce3f 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -25,6 +25,7 @@ import static androidx.lifecycle.LifecycleKt.getCoroutineScope; import static com.android.intentresolver.ChooserActionFactory.EDIT_SOURCE; import static com.android.intentresolver.Flags.shareouselUpdateExcludeComponentsExtra; import static com.android.intentresolver.Flags.fixShortcutsFlashing; +import static com.android.intentresolver.Flags.unselectFinalItem; import static com.android.intentresolver.ext.CreationExtrasExtKt.addDefaultArgs; import static com.android.intentresolver.profiles.MultiProfilePagerAdapter.PROFILE_PERSONAL; import static com.android.intentresolver.profiles.MultiProfilePagerAdapter.PROFILE_WORK; @@ -351,6 +352,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements if (mChooserServiceFeatureFlags.chooserPayloadToggling()) { mChooserHelper.setOnChooserRequestChanged(this::onChooserRequestChanged); mChooserHelper.setOnPendingSelection(this::onPendingSelection); + if (unselectFinalItem()) { + mChooserHelper.setOnHasSelections(this::onHasSelections); + } } } private int mInitialProfile = -1; @@ -705,7 +709,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements } private void onChooserRequestChanged(ChooserRequest chooserRequest) { - // intentional reference comparison if (mRequest == chooserRequest) { return; } @@ -724,6 +727,10 @@ public class ChooserActivity extends Hilt_ChooserActivity implements setTabsViewEnabled(false); } + private void onHasSelections(boolean hasSelections) { + mChooserMultiProfilePagerAdapter.setTargetsEnabled(hasSelections); + } + private void onAppTargetsLoaded(ResolverListAdapter listAdapter) { Log.d(TAG, "onAppTargetsLoaded(" + "listAdapter.userHandle=" + listAdapter.getUserHandle() + ")"); diff --git a/java/src/com/android/intentresolver/ChooserHelper.kt b/java/src/com/android/intentresolver/ChooserHelper.kt index 312911a6..7d382bfc 100644 --- a/java/src/com/android/intentresolver/ChooserHelper.kt +++ b/java/src/com/android/intentresolver/ChooserHelper.kt @@ -27,9 +27,12 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import com.android.intentresolver.Flags.unselectFinalItem import com.android.intentresolver.annotation.JavaInterop +import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ActivityResultRepository import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository +import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository import com.android.intentresolver.data.model.ChooserRequest import com.android.intentresolver.platform.GlobalSettings import com.android.intentresolver.ui.viewmodel.ChooserViewModel @@ -39,6 +42,8 @@ import com.android.intentresolver.validation.log import dagger.hilt.android.scopes.ActivityScoped import java.util.function.Consumer import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter @@ -46,6 +51,7 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch private const val TAG: String = "ChooserHelper" @@ -86,6 +92,7 @@ constructor( hostActivity: Activity, private val activityResultRepo: ActivityResultRepository, private val pendingSelectionCallbackRepo: PendingSelectionCallbackRepository, + private val selectionsRepo: PreviewSelectionsRepository, private val globalSettings: GlobalSettings, ) : DefaultLifecycleObserver { // This is guaranteed by Hilt, since only a ComponentActivity is injectable. @@ -98,6 +105,7 @@ constructor( var onChooserRequestChanged: Consumer<ChooserRequest> = Consumer {} /** Invoked when there are a new change to payload selection */ var onPendingSelection: Runnable = Runnable {} + var onHasSelections: Consumer<Boolean> = Consumer {} init { activity.lifecycle.addObserver(this) @@ -144,22 +152,40 @@ constructor( } activity.lifecycleScope.launch { - val hasPendingCallbackFlow = + val hasPendingIntentFlow = pendingSelectionCallbackRepo.pendingTargetIntent .map { it != null } .distinctUntilChanged() - .onEach { hasPendingCallback -> - if (hasPendingCallback) { + .onEach { hasPendingIntent -> + if (hasPendingIntent) { onPendingSelection.run() } } activity.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.request - .combine(hasPendingCallbackFlow) { request, hasPendingCallback -> - request to hasPendingCallback + val hasSelectionFlow = + if ( + unselectFinalItem() && + viewModel.previewDataProvider.previewType == + CONTENT_PREVIEW_PAYLOAD_SELECTION + ) { + selectionsRepo.selections + .map { it.isNotEmpty() } + .distinctUntilChanged() + .stateIn(scope = this) + .also { flow -> launch { flow.collect { onHasSelections.accept(it) } } } + } else { + MutableStateFlow(true).asStateFlow() } + val requestControlFlow = + hasSelectionFlow + .combine(hasPendingIntentFlow) { hasSelections, hasPendingIntent -> + hasSelections && !hasPendingIntent + } + .distinctUntilChanged() + viewModel.request + .combine(requestControlFlow) { request, isReady -> request to isReady } // only take ChooserRequest if there are no pending callbacks - .filter { !it.second } + .filter { it.second } .map { it.first } .distinctUntilChanged(areEquivalent = { old, new -> old === new }) .collect { onChooserRequestChanged.accept(it) } diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java index 26e2feeb..016eb714 100644 --- a/java/src/com/android/intentresolver/ChooserListAdapter.java +++ b/java/src/com/android/intentresolver/ChooserListAdapter.java @@ -18,7 +18,6 @@ package com.android.intentresolver; import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE; import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER; -import static com.android.intentresolver.util.graphics.SuspendedMatrixColorFilter.getSuspendedColorMatrix; import android.app.ActivityManager; import android.app.prediction.AppTarget; @@ -439,10 +438,7 @@ public class ChooserListAdapter extends ResolverListAdapter { } } - holder.bindIcon(info); - if (info.hasDisplayIcon() && !mTargetsEnabled) { - holder.icon.setColorFilter(getSuspendedColorMatrix()); - } + holder.bindIcon(info, mTargetsEnabled); if (mAnimateItems && info.hasDisplayIcon()) { mAnimationTracker.animateIcon(holder.icon, info); } diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java index 7ca1c724..fc5514b6 100644 --- a/java/src/com/android/intentresolver/ResolverListAdapter.java +++ b/java/src/com/android/intentresolver/ResolverListAdapter.java @@ -16,6 +16,7 @@ package com.android.intentresolver; +import static com.android.intentresolver.Flags.unselectFinalItem; import static com.android.intentresolver.util.graphics.SuspendedMatrixColorFilter.getSuspendedColorMatrix; import android.content.Context; @@ -973,13 +974,26 @@ public class ResolverListAdapter extends BaseAdapter { /** * Bind view holder to a TargetInfo. */ - public void bindIcon(TargetInfo info) { + public final void bindIcon(TargetInfo info) { + bindIcon(info, true); + } + + /** + * Bind view holder to a TargetInfo. + */ + public void bindIcon(TargetInfo info, boolean isEnabled) { Drawable displayIcon = info.getDisplayIconHolder().getDisplayIcon(); icon.setImageDrawable(displayIcon); - if (info.isSuspended()) { + if (info.isSuspended() || !isEnabled) { icon.setColorFilter(getSuspendedColorMatrix()); } else { icon.setColorFilter(null); + if (unselectFinalItem() && displayIcon != null) { + // For some reason, ImageView.setColorFilter() not always propagate the call + // to the drawable and the icon remains grayscale when rebound; reset the filter + // explicitly. + displayIcon.setColorFilter(null); + } } } } diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt index 21308341..059ee083 100644 --- a/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt +++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt @@ -36,4 +36,6 @@ interface HeadlineGenerator { fun getVideosHeadline(count: Int): String fun getFilesHeadline(count: Int): String + + fun getNotItemsSelectedHeadline(): String } diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt index e92d9bc6..822d3097 100644 --- a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt +++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt @@ -93,6 +93,9 @@ constructor( return getPluralString(R.string.sharing_files, count) } + override fun getNotItemsSelectedHeadline(): String = + context.getString(R.string.select_items_to_share) + private fun getPluralString(@StringRes templateResource: Int, count: Int): String { return PluralsMessageFormatter.format( context.resources, diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt index 81c56d1e..0688ce02 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt @@ -18,12 +18,12 @@ package com.android.intentresolver.contentpreview.payloadtoggle.data.repository import android.net.Uri import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel -import dagger.hilt.android.scopes.ViewModelScoped +import dagger.hilt.android.scopes.ActivityRetainedScoped import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow /** Stores set of selected previews. */ -@ViewModelScoped +@ActivityRetainedScoped class PreviewSelectionsRepository @Inject constructor() { val selections = MutableStateFlow(emptyMap<Uri, PreviewModel>()) } diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt index 97d9fa66..2d02e4fd 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt @@ -17,6 +17,7 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor import android.net.Uri +import com.android.intentresolver.Flags.unselectFinalItem import com.android.intentresolver.contentpreview.MimeTypeClassifier import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.TargetIntentModifier @@ -60,8 +61,12 @@ constructor( } fun unselect(model: PreviewModel) { - if (selectionsRepo.selections.value.size > 1) { - updateChooserRequest(selectionsRepo.selections.updateAndGet { it - model.uri }.values) + if (selectionsRepo.selections.value.size > 1 || unselectFinalItem()) { + selectionsRepo.selections + .updateAndGet { it - model.uri } + .values + .takeIf { it.isNotEmpty() } + ?.let { updateChooserRequest(it) } } } diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt index 93ac90db..f8cf243d 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt @@ -64,6 +64,7 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.intentresolver.Flags.unselectFinalItem import com.android.intentresolver.R import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.getOrDefault @@ -73,6 +74,7 @@ import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.Shar import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselViewModel import kotlin.math.abs import kotlin.math.min +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @Composable @@ -286,30 +288,46 @@ private fun ActionCarousel(viewModel: ShareouselViewModel) { val actions by viewModel.actions.collectAsStateWithLifecycle(initialValue = emptyList()) if (actions.isNotEmpty()) { Spacer(Modifier.height(16.dp)) - LazyRow( - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier.height(32.dp), - ) { - itemsIndexed(actions) { idx, actionViewModel -> - if (idx == 0) { - Spacer(Modifier.width(dimensionResource(R.dimen.chooser_edge_margin_normal))) - } - ShareouselAction( - label = actionViewModel.label, - onClick = { actionViewModel.onClicked() }, - ) { - actionViewModel.icon?.let { - Image( - icon = it, - modifier = Modifier.size(16.dp), - colorFilter = ColorFilter.tint(LocalContentColor.current) + val visibilityFlow = + if (unselectFinalItem()) { + viewModel.hasSelectedItems + } else { + MutableStateFlow(true) + } + val visibility by visibilityFlow.collectAsStateWithLifecycle(true) + val height = 32.dp + if (visibility) { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.height(height), + ) { + itemsIndexed(actions) { idx, actionViewModel -> + if (idx == 0) { + Spacer( + Modifier.width(dimensionResource(R.dimen.chooser_edge_margin_normal)) + ) + } + ShareouselAction( + label = actionViewModel.label, + onClick = { actionViewModel.onClicked() }, + ) { + actionViewModel.icon?.let { + Image( + icon = it, + modifier = Modifier.size(16.dp), + colorFilter = ColorFilter.tint(LocalContentColor.current) + ) + } + } + if (idx == actions.size - 1) { + Spacer( + Modifier.width(dimensionResource(R.dimen.chooser_edge_margin_normal)) ) } - } - if (idx == actions.size - 1) { - Spacer(Modifier.width(dimensionResource(R.dimen.chooser_edge_margin_normal))) } } + } else { + Spacer(modifier = Modifier.height(height)) } } } diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt index 6f8be1ff..9762794e 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt @@ -17,6 +17,7 @@ package com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel import android.util.Size import com.android.intentresolver.Flags +import com.android.intentresolver.Flags.unselectFinalItem import com.android.intentresolver.contentpreview.CachingImagePreviewImageLoader import com.android.intentresolver.contentpreview.HeadlineGenerator import com.android.intentresolver.contentpreview.ImageLoader @@ -58,6 +59,8 @@ data class ShareouselViewModel( val previews: Flow<PreviewsModel?>, /** List of action chips presented underneath Shareousel. */ val actions: Flow<List<ActionChipViewModel>>, + /** Indicates whether there are any selected items */ + val hasSelectedItems: Flow<Boolean>, /** Creates a [ShareouselPreviewViewModel] for a [PreviewModel] present in [previews]. */ val preview: ( @@ -104,10 +107,14 @@ object ShareouselViewModelModule { selectionInteractor.aggregateContentType.zip(selectionInteractor.amountSelected) { contentType, numItems -> - when (contentType) { - ContentType.Other -> headlineGenerator.getFilesHeadline(numItems) - ContentType.Image -> headlineGenerator.getImagesHeadline(numItems) - ContentType.Video -> headlineGenerator.getVideosHeadline(numItems) + if (unselectFinalItem() && numItems == 0) { + headlineGenerator.getNotItemsSelectedHeadline() + } else { + when (contentType) { + ContentType.Other -> headlineGenerator.getFilesHeadline(numItems) + ContentType.Image -> headlineGenerator.getImagesHeadline(numItems) + ContentType.Video -> headlineGenerator.getVideosHeadline(numItems) + } } }, metadataText = chooserRequestInteractor.metadataText, @@ -128,6 +135,7 @@ object ShareouselViewModelModule { } } }, + hasSelectedItems = selectionInteractor.selections.map { it.isNotEmpty() }, preview = { key, previewHeight, index, previewScope -> keySet.value?.maybeLoad(index) val previewInteractor = interactor.preview(key) diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractorTest.kt index 87db243d..c8242333 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractorTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractorTest.kt @@ -18,16 +18,24 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.interacto import android.content.Intent import android.net.Uri +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import com.android.intentresolver.Flags import com.android.intentresolver.contentpreview.mimetypeClassifier import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.previewSelectionsRepository import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel import com.android.intentresolver.util.runKosmosTest import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.first +import org.junit.Rule import org.junit.Test class SelectionInteractorTest { + @get:Rule val flagsRule = SetFlagsRule() + @Test + @DisableFlags(Flags.FLAG_UNSELECT_FINAL_ITEM) fun singleSelection_removalPrevented() = runKosmosTest { val initialPreview = PreviewModel( @@ -54,6 +62,33 @@ class SelectionInteractorTest { } @Test + @EnableFlags(Flags.FLAG_UNSELECT_FINAL_ITEM) + fun singleSelection_itemRemovedNoPendingIntentUpdates() = runKosmosTest { + val initialPreview = + PreviewModel( + uri = Uri.fromParts("scheme", "ssp", "fragment"), + mimeType = null, + order = 0 + ) + previewSelectionsRepository.selections.value = mapOf(initialPreview.uri to initialPreview) + + val underTest = + SelectionInteractor( + previewSelectionsRepository, + { Intent() }, + updateTargetIntentInteractor, + mimetypeClassifier, + ) + + assertThat(underTest.selections.first()).containsExactly(initialPreview.uri) + + underTest.unselect(initialPreview) + + assertThat(underTest.selections.first()).isEmpty() + assertThat(previewSelectionsRepository.selections.value).isEmpty() + } + + @Test fun multipleSelections_removalAllowed() = runKosmosTest { val first = PreviewModel( diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt index 1047d145..fc7ac751 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt @@ -347,6 +347,8 @@ class ShareouselViewModelTest { override fun getVideosHeadline(count: Int): String = "VIDEOS: $count" override fun getFilesHeadline(count: Int): String = "FILES: $count" + + override fun getNotItemsSelectedHeadline() = "Select items to share" } // instantiate the view model, and then runCurrent() so that it is fully hydrated before // starting the test |