diff options
Diffstat (limited to 'mobile/android/android-components/components/browser/tabstray')
20 files changed, 1206 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/browser/tabstray/README.md b/mobile/android/android-components/components/browser/tabstray/README.md new file mode 100644 index 0000000000..dc291eaf9f --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/README.md @@ -0,0 +1,19 @@ +# [Android Components](../../../README.md) > Browser > Tabstray + +A customizable tabs tray for browsers. + +## Usage + +### Setting up the dependency + +Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)): + +```Groovy +implementation "org.mozilla.components:browser-tabstray:{latest-version}" +``` + +## License + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/ diff --git a/mobile/android/android-components/components/browser/tabstray/build.gradle b/mobile/android/android-components/components/browser/tabstray/build.gradle new file mode 100644 index 0000000000..72e76233b4 --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/build.gradle @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + defaultConfig { + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + } + + lint { + warningsAsErrors true + abortOnError true + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + namespace 'mozilla.components.browser.tabstray' +} + +dependencies { + api project(':concept-tabstray') + + implementation project(':ui-icons') + implementation project(':ui-colors') + implementation project(':concept-base') + implementation project(':browser-state') + implementation project(':support-images') + implementation project(':support-ktx') + + implementation ComponentsDependencies.androidx_appcompat + implementation ComponentsDependencies.androidx_cardview + api ComponentsDependencies.androidx_recyclerview + + implementation ComponentsDependencies.kotlin_coroutines + + testImplementation project(':support-test') + + testImplementation ComponentsDependencies.androidx_test_core + testImplementation ComponentsDependencies.androidx_test_junit + testImplementation ComponentsDependencies.testing_robolectric +} + +apply from: '../../../android-lint.gradle' +apply from: '../../../publish.gradle' +ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) diff --git a/mobile/android/android-components/components/browser/tabstray/proguard-rules.pro b/mobile/android/android-components/components/browser/tabstray/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/AndroidManifest.xml b/mobile/android/android-components/components/browser/tabstray/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..1eccdee26a --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + + <application android:supportsRtl="true" /> +</manifest> diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/SelectableTabViewHolder.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/SelectableTabViewHolder.kt new file mode 100644 index 0000000000..8a9aa7b952 --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/SelectableTabViewHolder.kt @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.tabstray + +import android.view.View + +/** + * A contract for selectable ViewHolders for "tab" items. + */ +abstract class SelectableTabViewHolder(view: View) : TabViewHolder(view) { + /** + * Indicates the multi select state of tab item has changed based on [isSelected] . + */ + abstract fun showTabIsMultiSelectEnabled(selectedMaskView: View?, isSelected: Boolean) +} diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabTouchCallback.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabTouchCallback.kt new file mode 100644 index 0000000000..3c9ac7c0ef --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabTouchCallback.kt @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.tabstray + +import android.graphics.Canvas +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.browser.state.state.TabSessionState + +/** + * An [ItemTouchHelper.Callback] for support gestures on tabs in the tray. + * + * @param onRemoveTab A callback invoked when a tab is removed. + */ +open class TabTouchCallback( + private val onRemoveTab: (TabSessionState) -> Unit, +) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + with(viewHolder as TabViewHolder) { + tab?.let { onRemoveTab(it) } + } + } + + override fun onChildDraw( + c: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + dY: Float, + actionState: Int, + isCurrentlyActive: Boolean, + ) { + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { + // Alpha on an itemView being swiped should decrease to a min over a distance equal to + // the width of the item being swiped. + viewHolder.itemView.alpha = alphaForItemSwipe(dX, viewHolder.itemView.width) + } + + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + } + + /** + * Sets the alpha value for a swipe gesture. This is useful for inherited classes to provide their own values. + */ + open fun alphaForItemSwipe(dX: Float, distanceToAlphaMin: Int): Float { + return 1f + } + + override fun onMove(p0: RecyclerView, p1: RecyclerView.ViewHolder, p2: RecyclerView.ViewHolder): Boolean = false +} diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabViewHolder.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabViewHolder.kt new file mode 100644 index 0000000000..6c9a537af6 --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabViewHolder.kt @@ -0,0 +1,152 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.tabstray + +import android.content.res.ColorStateList +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.Dimension +import androidx.annotation.Dimension.Companion.DP +import androidx.annotation.VisibleForTesting +import androidx.appcompat.widget.AppCompatImageButton +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView +import mozilla.components.concept.base.images.ImageLoadRequest +import mozilla.components.concept.base.images.ImageLoader +import mozilla.components.support.ktx.android.util.dpToPx +import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl + +/** + * An abstract ViewHolder implementation for "tab" items. + */ +abstract class TabViewHolder(view: View) : RecyclerView.ViewHolder(view) { + abstract var tab: TabSessionState? + + /** + * Binds the ViewHolder to the `Tab`. + * @param tab the `Tab` used to bind the viewHolder. + * @param isSelected boolean to describe whether or not the `Tab` is selected. + * @param observable message bus to pass events to Observers of the TabsTray. + * // TODO fix comment + */ + abstract fun bind( + tab: TabSessionState, + isSelected: Boolean, + styling: TabsTrayStyling, + delegate: TabsTray.Delegate, + ) + + /** + * Ask for a partial update of the current tab. + * Allows for overriding the current behavior and add or remove the 'selected tab' UI decorator. + * + * When implementing this do not call super. + */ + open fun updateSelectedTabIndicator(showAsSelected: Boolean) { + // Not an abstract fun since not all clients of this library might be interested in this functionality. + // But throwing an exception if this is called without an actual implementation in clients. + throw UnsupportedOperationException("Method not yet implemented") + } +} + +/** + * The default implementation of `TabViewHolder` + */ +class DefaultTabViewHolder( + itemView: View, + private val thumbnailLoader: ImageLoader? = null, +) : TabViewHolder(itemView) { + @VisibleForTesting + internal val iconView: ImageView? = itemView.findViewById(R.id.mozac_browser_tabstray_icon) + + @VisibleForTesting + internal val titleView: TextView = itemView.findViewById(R.id.mozac_browser_tabstray_title) + + @VisibleForTesting + internal val closeView: AppCompatImageButton = itemView.findViewById(R.id.mozac_browser_tabstray_close) + private val thumbnailView: TabThumbnailView = itemView.findViewById(R.id.mozac_browser_tabstray_thumbnail) + private val urlView: TextView? = itemView.findViewById(R.id.mozac_browser_tabstray_url) + + override var tab: TabSessionState? = null + + @VisibleForTesting + internal var styling: TabsTrayStyling? = null + + /** + * Displays the data of the given session and notifies the given observable about events. + */ + override fun bind( + tab: TabSessionState, + isSelected: Boolean, + styling: TabsTrayStyling, + delegate: TabsTray.Delegate, + ) { + this.tab = tab + this.styling = styling + + val title = if (tab.content.title.isNotEmpty()) { + tab.content.title + } else { + tab.content.url + } + + titleView.text = title + urlView?.text = tab.content.url.tryGetHostFromUrl() + + itemView.setOnClickListener { + delegate.onTabSelected(tab) + } + + closeView.setOnClickListener { + delegate.onTabClosed(tab) + } + + updateSelectedTabIndicator(isSelected) + + // In the final else case, we have no cache or fresh screenshot; do nothing instead of clearing the image. + if (thumbnailLoader != null) { + val thumbnailSize = THUMBNAIL_SIZE.dpToPx(thumbnailView.context.resources.displayMetrics) + thumbnailLoader.loadIntoView( + thumbnailView, + ImageLoadRequest(id = tab.id, size = thumbnailSize, isPrivate = tab.content.private), + ) + } + + iconView?.setImageBitmap(tab.content.icon) + } + + override fun updateSelectedTabIndicator(showAsSelected: Boolean) { + if (showAsSelected) { + showItemAsSelected() + } else { + showItemAsNotSelected() + } + } + + @VisibleForTesting + internal fun showItemAsSelected() { + styling?.let { styling -> + titleView.setTextColor(styling.selectedItemTextColor) + itemView.setBackgroundColor(styling.selectedItemBackgroundColor) + closeView.imageTintList = ColorStateList.valueOf(styling.selectedItemTextColor) + } + } + + @VisibleForTesting + internal fun showItemAsNotSelected() { + styling?.let { styling -> + titleView.setTextColor(styling.itemTextColor) + itemView.setBackgroundColor(styling.itemBackgroundColor) + closeView.imageTintList = ColorStateList.valueOf(styling.itemTextColor) + } + } + + companion object { + @Dimension(unit = DP) + private const val THUMBNAIL_SIZE = 100 + } +} diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsAdapter.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsAdapter.kt new file mode 100644 index 0000000000..4df5bd934f --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsAdapter.kt @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.tabstray + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import mozilla.components.browser.state.state.TabPartition +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.concept.base.images.ImageLoader + +/** + * Function responsible for creating a `TabViewHolder` in the `TabsAdapter`. + */ +typealias ViewHolderProvider = (ViewGroup) -> TabViewHolder + +/** + * RecyclerView adapter implementation to display a list of tabs. + * + * @param thumbnailLoader an implementation of an [ImageLoader] for loading thumbnail images in the tabs tray. + * @param viewHolderProvider a function that creates a [TabViewHolder]. + * @param styling the default styling for the [TabsTrayStyling]. + * @param delegate a delegate to handle interactions in the tabs tray. + */ +open class TabsAdapter( + thumbnailLoader: ImageLoader? = null, + private val viewHolderProvider: ViewHolderProvider = { parent -> + DefaultTabViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.mozac_browser_tabstray_item, parent, false), + thumbnailLoader, + ) + }, + private val styling: TabsTrayStyling = TabsTrayStyling(), + private val delegate: TabsTray.Delegate, +) : ListAdapter<TabSessionState, TabViewHolder>(DiffCallback), TabsTray { + + private var selectedTabId: String? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabViewHolder { + return viewHolderProvider.invoke(parent) + } + + override fun onBindViewHolder(holder: TabViewHolder, position: Int) { + val tab = getItem(position) + + holder.bind(tab, tab.id == selectedTabId, styling, delegate) + } + + override fun onBindViewHolder( + holder: TabViewHolder, + position: Int, + payloads: List<Any>, + ) { + val tabs = currentList + if (tabs.isEmpty()) return + + if (payloads.isEmpty()) { + onBindViewHolder(holder, position) + return + } + + val tab = getItem(position) + + if (payloads.contains(PAYLOAD_HIGHLIGHT_SELECTED_ITEM) && tab.id == selectedTabId) { + holder.updateSelectedTabIndicator(true) + } else if (payloads.contains(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM) && tab.id == selectedTabId) { + holder.updateSelectedTabIndicator(false) + } + } + + override fun updateTabs(tabs: List<TabSessionState>, tabPartition: TabPartition?, selectedTabId: String?) { + this.selectedTabId = selectedTabId + + submitList(tabs) + } + + companion object { + /** + * Payload used in onBindViewHolder for a partial update of the current view. + * + * Signals that the currently selected tab should be highlighted. This is the default behavior. + */ + val PAYLOAD_HIGHLIGHT_SELECTED_ITEM: Int = R.id.payload_highlight_selected_item + + /** + * Payload used in onBindViewHolder for a partial update of the current view. + * + * Signals that the currently selected tab should NOT be highlighted. No tabs would appear as highlighted. + */ + val PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM: Int = R.id.payload_dont_highlight_selected_item + } + + private object DiffCallback : DiffUtil.ItemCallback<TabSessionState>() { + override fun areItemsTheSame(oldItem: TabSessionState, newItem: TabSessionState): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: TabSessionState, newItem: TabSessionState): Boolean { + return oldItem == newItem + } + } +} diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTray.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTray.kt new file mode 100644 index 0000000000..99a9f6b552 --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTray.kt @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.tabstray + +import mozilla.components.browser.state.state.TabPartition +import mozilla.components.browser.state.state.TabSessionState + +/** + * An interface to display a list of tabs. + */ +interface TabsTray { + + /** + * Interface to be implemented by classes that want to observe or react to the interactions on the tabs list. + */ + interface Delegate { + + /** + * A new tab has been selected. + */ + fun onTabSelected(tab: TabSessionState, source: String? = null) + + /** + * A tab has been closed. + */ + fun onTabClosed(tab: TabSessionState, source: String? = null) + } + + /** + * Called when the list of tabs are updated. + */ + fun updateTabs(tabs: List<TabSessionState>, tabPartition: TabPartition?, selectedTabId: String?) +} diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTrayStyling.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTrayStyling.kt new file mode 100644 index 0000000000..75808f77f3 --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTrayStyling.kt @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.tabstray + +const val DEFAULT_ITEM_BACKGROUND_COLOR = 0xFFFFFFFF.toInt() +const val DEFAULT_ITEM_BACKGROUND_SELECTED_COLOR = 0xFFFF45A1FF.toInt() +const val DEFAULT_ITEM_TEXT_COLOR = 0xFF111111.toInt() +const val DEFAULT_ITEM_TEXT_SELECTED_COLOR = 0xFFFFFFFF.toInt() + +/** + * Tabs tray styling for items in the [TabsAdapter]. If a custom [TabViewHolder] + * is used with [TabsAdapter.viewHolderProvider], the styling can be applied + * when [TabViewHolder.bind] is invoked. + * + * @property itemBackgroundColor the background color for all non-selected tabs. + * @property selectedItemBackgroundColor the background color for the selected tab. + * @property itemTextColor the text color for all non-selected tabs. + * @property selectedItemTextColor the text color for the selected tabs. + * @property itemUrlTextColor the URL text color for all non-selected tabs. + * @property selectedItemUrlTextColor the URL text color for the selected tab. + */ +data class TabsTrayStyling( + val itemBackgroundColor: Int = DEFAULT_ITEM_BACKGROUND_COLOR, + val selectedItemBackgroundColor: Int = DEFAULT_ITEM_BACKGROUND_SELECTED_COLOR, + val itemTextColor: Int = DEFAULT_ITEM_TEXT_COLOR, + val selectedItemTextColor: Int = DEFAULT_ITEM_TEXT_SELECTED_COLOR, + val itemUrlTextColor: Int = DEFAULT_ITEM_TEXT_COLOR, + val selectedItemUrlTextColor: Int = DEFAULT_ITEM_TEXT_SELECTED_COLOR, +) diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailView.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailView.kt new file mode 100644 index 0000000000..bbb2db2d22 --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailView.kt @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.tabstray.thumbnail + +import android.content.Context +import android.util.AttributeSet +import androidx.annotation.VisibleForTesting +import androidx.appcompat.widget.AppCompatImageView + +class TabThumbnailView(context: Context, attrs: AttributeSet) : AppCompatImageView(context, attrs) { + + init { + scaleType = ScaleType.MATRIX + } + + @VisibleForTesting + public override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean { + val result = super.setFrame(l, t, r, b) + + val matrix = imageMatrix + val scaleFactor = if (drawable != null) { + width / drawable.intrinsicWidth.toFloat() + } else { + 1F + } + matrix.setScale(scaleFactor, scaleFactor, 0f, 0f) + imageMatrix = matrix + + return result + } +} diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/res/layout/mozac_browser_tabstray_item.xml b/mobile/android/android-components/components/browser/tabstray/src/main/res/layout/mozac_browser_tabstray_item.xml new file mode 100644 index 0000000000..fe701265b3 --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/main/res/layout/mozac_browser_tabstray_item.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="150dp" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_margin="4dp" + android:clickable="true" + android:focusable="true"> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ImageView + android:id="@+id/mozac_browser_tabstray_icon" + android:layout_width="36dp" + android:layout_height="36dp" + android:importantForAccessibility="no" + android:padding="4dp" /> + + <TextView + android:id="@+id/mozac_browser_tabstray_title" + android:layout_width="match_parent" + android:layout_height="36dp" + android:layout_alignParentTop="true" + android:layout_marginStart="42dp" + android:layout_toStartOf="@id/mozac_browser_tabstray_close" + android:ellipsize="end" + android:lines="1" + android:padding="8dp" + tools:text="Mozilla" /> + + <TextView + android:id="@+id/mozac_browser_tabstray_url" + android:layout_width="match_parent" + android:layout_height="36dp" + android:layout_alignParentTop="true" + android:layout_marginStart="42dp" + android:layout_toStartOf="@id/mozac_browser_tabstray_close" + android:ellipsize="end" + android:lines="1" + android:padding="8dp" + android:visibility="gone" + tools:text="www.mozilla.org" /> + + <androidx.appcompat.widget.AppCompatImageButton + android:id="@+id/mozac_browser_tabstray_close" + android:layout_width="36dp" + android:layout_height="36dp" + android:layout_alignParentEnd="true" + android:layout_alignParentTop="true" + android:contentDescription="@string/mozac_browser_tabstray_close_tab" + android:background="?android:attr/selectableItemBackgroundBorderless" + app:srcCompat="@drawable/mozac_ic_cross_20" /> + + <mozilla.components.browser.tabstray.thumbnail.TabThumbnailView + android:id="@+id/mozac_browser_tabstray_thumbnail" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_below="@id/mozac_browser_tabstray_title" + android:contentDescription="@string/mozac_browser_tabstray_open_tab" /> + + </RelativeLayout> +</androidx.cardview.widget.CardView> diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/res/values/ids.xml b/mobile/android/android-components/components/browser/tabstray/src/main/res/values/ids.xml new file mode 100644 index 0000000000..5926a7437d --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/main/res/values/ids.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<resources> + <item name="payload_highlight_selected_item" type="id"/> + <item name="payload_dont_highlight_selected_item" type="id"/> +</resources> diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/res/values/mozac_browser_tabstray_strings.xml b/mobile/android/android-components/components/browser/tabstray/src/main/res/values/mozac_browser_tabstray_strings.xml new file mode 100644 index 0000000000..acd1b1256d --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/main/res/values/mozac_browser_tabstray_strings.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<resources> + + <!-- Content description (not visible, for screen readers etc.): Description for button closing a tab. --> + <string name="mozac_browser_tabstray_close_tab">Close Tab</string> + <string name="mozac_browser_tabstray_open_tab">Open Tab</string> +</resources> diff --git a/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/DefaultTabViewHolderTest.kt b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/DefaultTabViewHolderTest.kt new file mode 100644 index 0000000000..833a3f1358 --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/DefaultTabViewHolderTest.kt @@ -0,0 +1,223 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.tabstray + +import android.content.res.ColorStateList +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.createTab +import mozilla.components.concept.base.images.ImageLoadRequest +import mozilla.components.concept.base.images.ImageLoader +import mozilla.components.support.test.any +import mozilla.components.support.test.eq +import mozilla.components.support.test.mock +import mozilla.components.support.test.nullable +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertSame +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class DefaultTabViewHolderTest { + + @Test + fun `URL from session is assigned to view`() { + val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null) + val titleView = view.findViewById<TextView>(R.id.mozac_browser_tabstray_title) + val urlView = view.findViewById<TextView>(R.id.mozac_browser_tabstray_url) + + val holder = DefaultTabViewHolder(view) + + assertEquals("", titleView.text) + + val session = createTab(id = "a", url = "https://www.mozilla.org") + + holder.bind(session, isSelected = false, styling = mock(), mock()) + + assertEquals("https://www.mozilla.org", titleView.text) + assertEquals("www.mozilla.org", urlView.text) + } + + @Test + fun `URL text is set to tab URL when exception is thrown`() { + val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null) + val urlView = view.findViewById<TextView>(R.id.mozac_browser_tabstray_url) + val holder = DefaultTabViewHolder(view) + val session = createTab(id = "a", url = "about:home") + + holder.bind(session, isSelected = false, styling = mock(), mock()) + + assertEquals("about:home", urlView.text) + } + + @Test + fun `observer gets notified if item is clicked`() { + val delegate: TabsTray.Delegate = mock() + + val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null) + val holder = DefaultTabViewHolder(view) + + val session = createTab(url = "https://www.mozilla.org", id = "a") + holder.bind(session, isSelected = false, styling = mock(), delegate) + + view.performClick() + + verify(delegate).onTabSelected(session) + } + + @Test + fun `observer gets notified if tab gets closed`() { + val delegate: TabsTray.Delegate = mock() + + val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null) + val holder = DefaultTabViewHolder(view) + + val session = createTab(url = "https://www.mozilla.org", id = "a") + holder.bind(session, isSelected = true, styling = mock(), delegate) + + view.findViewById<View>(R.id.mozac_browser_tabstray_close).performClick() + + verify(delegate).onTabClosed(session) + } + + @Test + fun `url from session is displayed by default`() { + val delegate: TabsTray.Delegate = mock() + val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null) + val holder = DefaultTabViewHolder(view) + + val session = createTab(url = "https://www.mozilla.org", id = "a") + val titleView = holder.itemView.findViewById<TextView>(R.id.mozac_browser_tabstray_title) + val urlView = view.findViewById<TextView>(R.id.mozac_browser_tabstray_url) + + holder.bind(session, isSelected = true, styling = mock(), delegate) + + assertEquals(session.content.url, titleView.text) + assertEquals("www.mozilla.org", urlView.text) + } + + @Test + fun `title from session is displayed if available`() { + val delegate: TabsTray.Delegate = mock() + val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null) + val holder = DefaultTabViewHolder(view) + + val session = createTab(url = "https://www.mozilla.org", title = "Mozilla Firefox", id = "a") + val titleView = holder.itemView.findViewById<TextView>(R.id.mozac_browser_tabstray_title) + val urlView = view.findViewById<TextView>(R.id.mozac_browser_tabstray_url) + + holder.bind(session, isSelected = true, styling = mock(), delegate) + assertEquals("Mozilla Firefox", titleView.text) + assertEquals("www.mozilla.org", urlView.text) + } + + @Test + fun `thumbnail is set from loader`() { + val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null) + val loader: ImageLoader = mock() + val viewHolder = DefaultTabViewHolder(view, loader) + val tab = createTab(id = "123", url = "https://example.com") + + viewHolder.bind(tab, false, mock(), mock()) + + verify(loader).loadIntoView(any(), eq(ImageLoadRequest("123", 100, false)), nullable(), nullable()) + } + + @Test + fun `thumbnailView does not change when there is no cache or new thumbnail`() { + val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null) + val viewHolder = DefaultTabViewHolder(view) + val tab = createTab(id = "123", url = "https://example.com") + val thumbnailView = view.findViewById<ImageView>(R.id.mozac_browser_tabstray_thumbnail) + + thumbnailView.setImageBitmap(mock()) + val drawable = thumbnailView.drawable + + viewHolder.bind(tab, false, mock(), mock()) + + assertEquals(drawable, thumbnailView.drawable) + } + + @Test + fun `bind sets the values for this instance's Tab and TabsTrayStyling properties`() { + val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null) + val viewHolder = DefaultTabViewHolder(view) + val tab = createTab(id = "123", url = "https://example.com") + val styling: TabsTrayStyling = mock() + + assertNull(viewHolder.tab) + assertNull(viewHolder.styling) + + viewHolder.bind(tab, false, styling, mock()) + + assertSame(tab, viewHolder.tab) + assertSame(styling, viewHolder.styling) + } + + @Test + fun `bind shows an item as selected or not`() { + val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null) + val tab = createTab(id = "123", url = "https://example.com") + val viewHolder = spy(DefaultTabViewHolder(view)) + + viewHolder.bind(tab, false, mock(), mock()) + verify(viewHolder).updateSelectedTabIndicator(false) + + viewHolder.bind(tab, true, mock(), mock()) + verify(viewHolder).updateSelectedTabIndicator(true) + } + + @Test + fun `updateSelectedTabIndicator should further delegate to the appropriate method`() { + val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null) + val viewHolder = spy(DefaultTabViewHolder(view)) + + viewHolder.updateSelectedTabIndicator(showAsSelected = true) + verify(viewHolder).showItemAsSelected() + + viewHolder.updateSelectedTabIndicator(showAsSelected = false) + verify(viewHolder).showItemAsNotSelected() + } + + @Test + fun `showItemAsSelected should use TabsTrayStyling for indicating that an item is currently selected`() { + val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null) + val viewHolder = spy(DefaultTabViewHolder(view)) + val tab = createTab(id = "123", url = "https://example.com") + val styling = TabsTrayStyling() + + // Need to be called first to set the styling for this holder + viewHolder.bind(tab, false, TabsTrayStyling(), mock()) + viewHolder.updateSelectedTabIndicator(true) + + assertEquals(styling.selectedItemTextColor, viewHolder.titleView.textColors.defaultColor) + assertEquals(styling.selectedItemBackgroundColor, (viewHolder.itemView.background as ColorDrawable).color) + assertEquals(ColorStateList.valueOf(styling.selectedItemTextColor), viewHolder.closeView.imageTintList) + } + + @Test + fun `showItemAsSelected should use TabsTrayStyling for indicating that an item is not currently selected`() { + val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null) + val viewHolder = spy(DefaultTabViewHolder(view)) + val tab = createTab(id = "123", url = "https://example.com") + val styling = TabsTrayStyling() + + // Need to be called first to set the styling for this holder + viewHolder.bind(tab, true, TabsTrayStyling(), mock()) + viewHolder.updateSelectedTabIndicator(false) + + assertEquals(styling.itemTextColor, viewHolder.titleView.textColors.defaultColor) + assertEquals(styling.itemBackgroundColor, (viewHolder.itemView.background as ColorDrawable).color) + assertEquals(ColorStateList.valueOf(styling.itemTextColor), viewHolder.closeView.imageTintList) + } +} diff --git a/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabTouchCallbackTest.kt b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabTouchCallbackTest.kt new file mode 100644 index 0000000000..3df5abdead --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabTouchCallbackTest.kt @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.tabstray + +import android.view.LayoutInflater +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.`when` + +@RunWith(AndroidJUnit4::class) +class TabTouchCallbackTest { + + @Test + fun `onSwiped notifies observers`() { + var onTabClosedWasCalled = false + + val onTabClosed: (TabSessionState) -> Unit = { + onTabClosedWasCalled = true + } + val touchCallback = TabTouchCallback(onTabClosed) + val viewHolder: TabViewHolder = mock() + + touchCallback.onSwiped(viewHolder, 0) + + assertFalse(onTabClosedWasCalled) + + // With a session available. + `when`(viewHolder.tab).thenReturn(mock()) + + touchCallback.onSwiped(viewHolder, 0) + + assertTrue(onTabClosedWasCalled) + } + + @Test + fun `onChildDraw alters alpha of ViewHolder on swipe gesture`() { + val view = LayoutInflater.from(testContext).inflate(R.layout.mozac_browser_tabstray_item, null) + val holder = DefaultTabViewHolder(view) + val callback = TabTouchCallback(mock()) + + holder.itemView.alpha = 0f + + callback.onChildDraw(mock(), mock(), holder, 0f, 0f, ItemTouchHelper.ACTION_STATE_DRAG, true) + + assertEquals(0f, holder.itemView.alpha) + + callback.onChildDraw(mock(), mock(), holder, 0f, 0f, ItemTouchHelper.ACTION_STATE_SWIPE, true) + + assertEquals(1f, holder.itemView.alpha) + } + + @Test + fun `alpha default is full`() { + val touchCallback = TabTouchCallback(mock()) + assertEquals(1f, touchCallback.alphaForItemSwipe(0f, 0)) + } + + @Test + fun `onMove is not implemented`() { + val touchCallback = TabTouchCallback(mock()) + assertFalse(touchCallback.onMove(mock(), mock(), mock())) + } +} diff --git a/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabViewHolderTest.kt b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabViewHolderTest.kt new file mode 100644 index 0000000000..4a69343cb1 --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabViewHolderTest.kt @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.tabstray + +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import junit.framework.TestCase +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.support.test.expectException +import mozilla.components.support.test.robolectric.testContext +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TabViewHolderTest : TestCase() { + + @Test + fun `updateSelectedTabIndicator needs to have a provided implementation`() { + val simpleTabViewHolder = object : TabViewHolder(View(testContext)) { + override var tab: TabSessionState? = null + override fun bind( + tab: TabSessionState, + isSelected: Boolean, + styling: TabsTrayStyling, + delegate: TabsTray.Delegate, + ) { /* noop */ } + } + + expectException(UnsupportedOperationException::class) { + simpleTabViewHolder.updateSelectedTabIndicator(true) + } + } + + @Test + fun `updateSelectedTabIndicator with a provided implementation just works`() { + val tabViewHolder = object : TabViewHolder(View(testContext)) { + override var tab: TabSessionState? = null + override fun bind( + tab: TabSessionState, + isSelected: Boolean, + styling: TabsTrayStyling, + delegate: TabsTray.Delegate, + ) { /* noop */ } + override fun updateSelectedTabIndicator(showAsSelected: Boolean) { /* noop */ } + } + + // Simply test that this would not fail the test like it would happen above. + tabViewHolder.updateSelectedTabIndicator(true) + } +} diff --git a/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabsAdapterTest.kt b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabsAdapterTest.kt new file mode 100644 index 0000000000..20fa366b52 --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabsAdapterTest.kt @@ -0,0 +1,161 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.tabstray + +import android.view.View +import android.widget.FrameLayout +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM +import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify + +private class TestTabViewHolder(view: View) : TabViewHolder(view) { + override var tab: TabSessionState? = null + override fun bind( + tab: TabSessionState, + isSelected: Boolean, + styling: TabsTrayStyling, + delegate: TabsTray.Delegate, + ) { // noop + } + + override fun updateSelectedTabIndicator(showAsSelected: Boolean) { // noop + } +} + +@RunWith(AndroidJUnit4::class) +class TabsAdapterTest { + + @Test + fun `onCreateViewHolder will create a DefaultTabViewHolder`() { + val adapter = TabsAdapter(delegate = mock()) + + val type = adapter.onCreateViewHolder(FrameLayout(testContext), 0) + + assertTrue(type is DefaultTabViewHolder) + } + + @Test + fun `onCreateViewHolder will create whatever TabViewHolder is provided`() { + val adapter = TabsAdapter( + viewHolderProvider = { _ -> TestTabViewHolder(View(testContext)) }, + delegate = mock(), + ) + + val type = adapter.onCreateViewHolder(FrameLayout(testContext), 0) + + assertTrue(type is TestTabViewHolder) + } + + @Test + fun `itemCount will reflect number of sessions`() { + val adapter = TabsAdapter(delegate = mock()) + assertEquals(0, adapter.itemCount) + + adapter.updateTabs( + listOf( + createTab(id = "A", url = "https://www.mozilla.org"), + createTab(id = "B", url = "https://www.firefox.com"), + ), + tabPartition = null, + selectedTabId = "A", + ) + assertEquals(2, adapter.itemCount) + } + + @Test + fun `onBindViewHolder calls bind on matching holder`() { + val styling = TabsTrayStyling() + val delegate = mock<TabsTray.Delegate>() + val adapter = TabsAdapter(delegate = delegate, styling = styling) + + val holder: TabViewHolder = mock() + + val tab = createTab(id = "A", url = "https://www.mozilla.org") + + adapter.updateTabs( + listOf(tab), + null, + "A", + ) + + adapter.onBindViewHolder(holder, 0) + + verify(holder).bind(tab, true, styling, delegate) + } + + @Test + fun `onBindViewHolder will use payloads to indicate if this item is selected`() { + val adapter = TabsAdapter(delegate = mock()) + val holder = spy(TestTabViewHolder(View(testContext))) + val tab = createTab(id = "A", url = "https://www.mozilla.org") + + adapter.updateTabs(listOf(mock(), tab), tabPartition = null, selectedTabId = "A") + + adapter.onBindViewHolder(holder, 0, listOf(PAYLOAD_HIGHLIGHT_SELECTED_ITEM)) + verify(holder, never()).updateSelectedTabIndicator(ArgumentMatchers.anyBoolean()) + + adapter.onBindViewHolder(holder, 1, listOf(PAYLOAD_HIGHLIGHT_SELECTED_ITEM)) + verify(holder).updateSelectedTabIndicator(true) + } + + @Test + fun `onBindViewHolder will use payloads to indicate if this item is not selected`() { + val adapter = TabsAdapter(delegate = mock()) + val holder = spy(TestTabViewHolder(View(testContext))) + val tab = createTab(id = "A", url = "https://www.mozilla.org") + adapter.updateTabs(listOf(mock(), tab), tabPartition = null, selectedTabId = "A") + + adapter.onBindViewHolder(holder, 0, listOf(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM)) + verify(holder, never()).updateSelectedTabIndicator(ArgumentMatchers.anyBoolean()) + + adapter.onBindViewHolder(holder, 1, listOf(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM)) + verify(holder).updateSelectedTabIndicator(false) + } + + @Test + fun `onBindViewHolder with payloads will return early if there are currently no open tabs`() { + val adapter = TabsAdapter(delegate = mock()) + val holder = TestTabViewHolder(View(testContext)) + val payloads = spy(arrayListOf(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM)) + + adapter.onBindViewHolder(holder, 0, payloads) + // verify that calls we expect further down are not happening after the null check + verify(payloads, never()).isEmpty() + verify(payloads, never()).contains(ArgumentMatchers.anyInt()) + + adapter.updateTabs(emptyList(), tabPartition = null, selectedTabId = null) + adapter.onBindViewHolder(holder, 0, payloads) + // verify that calls we expect further down are not happening after the null check + verify(payloads, never()).isEmpty() + verify(payloads, never()).contains(ArgumentMatchers.anyInt()) + } + + @Test + fun `onBindViewHolder with empty payloads will call onBindViewHolder and return early for a full bind`() { + val adapter = TabsAdapter(delegate = mock()) + val holder = TestTabViewHolder(View(testContext)) + val emptyPayloads = spy(arrayListOf<String>()) + + adapter.updateTabs(listOf(mock()), tabPartition = null, selectedTabId = null) + + adapter.onBindViewHolder(holder, 0, emptyPayloads) + + verify(emptyPayloads).isEmpty() + // verify that calls we expect further down are not happening after the isEmpty check + verify(emptyPayloads, never()).contains(ArgumentMatchers.anyString()) + } +} diff --git a/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailViewTest.kt b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailViewTest.kt new file mode 100644 index 0000000000..9842186874 --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailViewTest.kt @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.tabstray.thumbnail + +import android.graphics.Matrix +import android.graphics.drawable.Drawable +import android.widget.ImageView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.robolectric.Robolectric.buildAttributeSet + +@RunWith(AndroidJUnit4::class) +class TabThumbnailViewTest { + + @Test + fun `view should always use Matrix ScaleType`() { + val view = TabThumbnailView(testContext, emptyAttributeSet()) + assertEquals(ImageView.ScaleType.MATRIX, view.scaleType) + } + + @Test + fun `view updates matrix when changed`() { + val view = TabThumbnailView(testContext, emptyAttributeSet()) + val matrix = view.imageMatrix + val drawable: Drawable = mock() + + `when`(drawable.intrinsicWidth).thenReturn(5) + `when`(drawable.intrinsicHeight).thenReturn(5) + + view.setImageDrawable(drawable) + view.setFrame(5, 5, 5, 5) + + val matrix2 = view.imageMatrix + + assertNotEquals(matrix, matrix2) + } + + @Test + fun `view updates don't change matrix if no changes to frame`() { + val view = TabThumbnailView(testContext, emptyAttributeSet()) + val drawable: Drawable = mock() + + `when`(drawable.intrinsicWidth).thenReturn(5) + `when`(drawable.intrinsicHeight).thenReturn(5) + + view.setImageDrawable(drawable) + view.setFrame(5, 5, 5, 5) + + val matrix = view.imageMatrix + + view.setFrame(5, 5, 5, 5) + + val matrix2 = view.imageMatrix + + assertEquals(matrix, matrix2) + } + + @Test + fun `view scaleFactor does not change if there is no drawable`() { + val view = spy(TabThumbnailView(testContext, emptyAttributeSet())) + val matrix: Matrix = spy(Matrix()) + + `when`(view.imageMatrix).thenReturn(matrix) + + view.setFrame(5, 5, 5, 5) + + verify(matrix).setScale(1f, 1f, 0f, 0f) + } +} + +private fun emptyAttributeSet() = buildAttributeSet().build() diff --git a/mobile/android/android-components/components/browser/tabstray/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/android-components/components/browser/tabstray/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..cf1c399ea8 --- /dev/null +++ b/mobile/android/android-components/components/browser/tabstray/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1,2 @@ +mock-maker-inline +// This allows mocking final classes (classes are final by default in Kotlin) |