diff --git a/.buildconfig.yml b/.buildconfig.yml
index 52a97a3d876..ff1b8c0a179 100644
--- a/.buildconfig.yml
+++ b/.buildconfig.yml
@@ -1,6 +1,6 @@
# Please keep this version in sync with version.txt
# version.txt should be the new source of truth for version numbers
-componentsVersion: 101.0.0
+componentsVersion: 103.0.0
# Please add a treeherder group in taskcluster/ci/config.yml if you add a new project here.
projects:
compose-awesomebar:
@@ -23,8 +23,8 @@ projects:
path: components/concept/awesomebar
description: 'An abstract definition of an awesomebar component.'
publish: true
- feature-biometric-prompt:
- path: components/feature/biometric-prompt
+ lib-auth:
+ path: components/lib/biometric-prompt
description: 'Component for authentication using biometric'
publish: true
concept-base:
diff --git a/.github/ISSUE_TEMPLATE/---performance-issue.md b/.github/ISSUE_TEMPLATE/---performance-issue.md
new file mode 100644
index 00000000000..c85ebdf9b18
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/---performance-issue.md
@@ -0,0 +1,20 @@
+---
+name: "⌛ Performance issue"
+about: Create a performance issue if the app is slow or it uses too much memory, disk space, battery, or network data
+title: ""
+labels: "performance"
+assignees: ''
+
+---
+
+## Steps to reproduce
+
+### Expected behavior
+
+### Actual behavior
+
+### Device information
+
+* Android device: ?
+* App (fenix, focus, ...): ?
+* App version: ?
diff --git a/README.md b/README.md
index 5c7e69a8842..5edec069552 100644
--- a/README.md
+++ b/README.md
@@ -71,7 +71,7 @@ High-level components for building browser(-like) apps.
* 🔵 [**Icons**](components/browser/icons/README.md) - A component for loading and storing website icons (like [Favicons](https://en.wikipedia.org/wiki/Favicon)).
-* ⚪ [**Menu**](components/browser/menu/README.md) - A generic menu with customizable items primarily for browser toolbars.
+* 🔵 [**Menu**](components/browser/menu/README.md) - A generic menu with customizable items primarily for browser toolbars.
* ⚪ [**Menu 2**](components/browser/menu2/README.md) - A generic menu with customizable items primarily for browser toolbars.
@@ -81,9 +81,9 @@ High-level components for building browser(-like) apps.
* 🔵 [**Storage-Sync**](components/browser/storage-sync/README.md) - A syncable implementation of browser storage backed by [application-services' Places lib](https://github.com/mozilla/application-services).
-* ⚪ [**Tabstray**](components/browser/tabstray/README.md) - A customizable tabs tray for browsers.
+* 🔵 [**Tabstray**](components/browser/tabstray/README.md) - A customizable tabs tray for browsers.
-* 🔴 [**Thumbnails**](components/browser/thumbnails/README.md) - A component for loading and storing website thumbnails (screenshot of the website).
+* 🔵 [**Thumbnails**](components/browser/thumbnails/README.md) - A component for loading and storing website thumbnails (screenshot of the website).
* 🔵 [**Toolbar**](components/browser/toolbar/README.md) - A customizable toolbar for browsers.
@@ -101,7 +101,7 @@ _API contracts and abstraction layers for browser components._
* 🔵 [**Storage**](components/concept/storage/README.md) - Abstract definition of a browser storage component.
-* ⚪ [**Tabstray**](components/concept/tabstray/README.md) - Abstract definition of a tabs tray component.
+* 🔵 [**Tabstray**](components/concept/tabstray/README.md) - Abstract definition of a tabs tray component.
* 🔵 [**Toolbar**](components/concept/toolbar/README.md) - Abstract definition of a browser toolbar component.
@@ -113,7 +113,7 @@ _Combined components to implement feature-specific use cases._
* 🔵 [**Accounts Push**](components/feature/accounts-push/README.md) - Feature of use cases for FxA Account that work with push support.
-* 🔴 [**Autofill**](components/feature/autofill/README.md) - A component that provides support for Android's Autofill framework.
+* 🔵 [**Autofill**](components/feature/autofill/README.md) - A component that provides support for Android's Autofill framework.
* 🔵 [**Awesomebar**](components/feature/awesomebar/README.md) - A component that connects a [concept-awesomebar](components/concept/awesomebar/README.md) implementation to a [concept-toolbar](components/concept/toolbar/README.md) implementation and provides implementations of various suggestion providers.
@@ -123,7 +123,7 @@ _Combined components to implement feature-specific use cases._
* 🔵 [**Custom Tabs**](components/feature/customtabs/README.md) - A component for providing [Custom Tabs](https://developer.chrome.com/multidevice/android/customtabs) functionality in browsers.
-* ⚪ [**Downloads**](components/feature/downloads/README.md) - A component to perform downloads using the [Android downloads manager](https://developer.android.com/reference/android/app/DownloadManager).
+* 🔵 [**Downloads**](components/feature/downloads/README.md) - A component to perform downloads using the [Android downloads manager](https://developer.android.com/reference/android/app/DownloadManager).
* 🔵 [**Intent**](components/feature/intent/README.md) - A component that provides intent processing functionality by combining various other feature modules.
@@ -155,11 +155,11 @@ _Combined components to implement feature-specific use cases._
* 🔵 [**Find In Page**](components/feature/findinpage/README.md) - A component that provides an UI widget for [find in page functionality](https://support.mozilla.org/en-US/kb/search-contents-current-page-text-or-links).
-* ⚪ [**Remote Tabs**](components/feature/remotetabs/README.md) - Feature that provides access to other device's tabs in the same account.
+* 🔵 [**Remote Tabs**](components/feature/remotetabs/README.md) - Feature that provides access to other device's tabs in the same account.
* 🔵 [**Site Permissions**](components/feature/sitepermissions/README.md) - A feature for showing site permission request prompts.
-* ⚪ [**WebAuthn**](components/feature/webauthn/README.md) - A feature that provides WebAuthn functionality for supported engines.
+* 🔵 [**WebAuthn**](components/feature/webauthn/README.md) - A feature that provides WebAuthn functionality for supported engines.
* 🔵 [**Web Notifications**](components/feature/webnotifications/README.md) - A component for displaying web notifications.
@@ -191,7 +191,7 @@ _Components and libraries to interact with backend services._
* 🔵 [**Firefox Sync - Logins**](components/service/sync-logins/README.md) - A library for integrating with Firefox Sync - Logins.
-* 🔴 [**Firefox Sync - Autofill**](components/service/sync-autofill/README.md) - A library for integrating with Firefox Sync - Autofill.
+* 🔵 [**Firefox Sync - Autofill**](components/service/sync-autofill/README.md) - A library for integrating with Firefox Sync - Autofill.
* 🔵 [**Glean**](components/service/glean/README.md) - A client-side telemetry SDK for collecting metrics and sending them to Mozilla's telemetry service (eventually replacing [service-telemetry](components/service/telemetry/README.md)).
@@ -199,9 +199,9 @@ _Components and libraries to interact with backend services._
* 🔴 [**Nimbus**](components/service/nimbus/README.md) - A wrapper for the Nimbus SDK.
-* 🔴 [**Pocket**](components/service/pocket/README.md) - A library for communicating with the Pocket API.
+* 🔵 [**Pocket**](components/service/pocket/README.md) - A library for communicating with the Pocket API.
-* 🔴 [**Contile**](components/service/contile/README.md) - A library for communicating with the Contile services API.
+* 🔵 [**Contile**](components/service/contile/README.md) - A library for communicating with the Contile services API.
## Support
diff --git a/build.gradle b/build.gradle
index 5a16523eaad..5a1bb4a5d2c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -109,6 +109,12 @@ subprojects {
}
}
+ // Allow local Glean substitution in each subproject.
+ if (gradle.hasProperty('localProperties.autoPublish.glean.dir')) {
+ ext.gleanSrcDir = gradle."localProperties.autoPublish.glean.dir"
+ apply from: "${rootProject.projectDir}/${gleanSrcDir}/build-scripts/substitute-local-glean.gradle"
+ }
+
if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopsrcdir')) {
if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopobjdir')) {
ext.topobjdir = gradle."localProperties.dependencySubstitutions.geckoviewTopobjdir"
diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt
index 2acbdd850aa..e8682800f0e 100644
--- a/buildSrc/src/main/java/Dependencies.kt
+++ b/buildSrc/src/main/java/Dependencies.kt
@@ -8,7 +8,7 @@
// Synchronized version numbers for dependencies used by (some) modules
object Versions {
const val kotlin = "1.6.10"
- const val coroutines = "1.5.2"
+ const val coroutines = "1.6.1"
const val junit = "4.12"
const val robolectric = "4.7.3"
@@ -24,14 +24,14 @@ object Versions {
const val detekt = "1.19.0"
const val sentry_legacy = "1.7.21"
- const val sentry_latest = "5.6.1"
+ const val sentry_latest = "5.7.3"
const val okhttp = "3.13.1"
const val zxing = "3.3.0"
const val jna = "5.8.0"
const val disklrucache = "2.0.2"
const val leakcanary = "2.8.1"
- const val mozilla_appservices = "91.1.2"
+ const val mozilla_appservices = "93.2.2"
const val mozilla_glean = "44.1.1"
diff --git a/buildSrc/src/main/java/Gecko.kt b/buildSrc/src/main/java/Gecko.kt
index 5686fc92976..25f32bc5a54 100644
--- a/buildSrc/src/main/java/Gecko.kt
+++ b/buildSrc/src/main/java/Gecko.kt
@@ -9,7 +9,7 @@ object Gecko {
/**
* GeckoView Version.
*/
- const val version = "101.0.20220426094609"
+ const val version = "103.0.20220605065813"
/**
* GeckoView channel
diff --git a/components/browser/awesomebar/src/main/res/values-ast/strings.xml b/components/browser/awesomebar/src/main/res/values-ast/strings.xml
index 1cd6d971f70..be4ad492455 100644
--- a/components/browser/awesomebar/src/main/res/values-ast/strings.xml
+++ b/components/browser/awesomebar/src/main/res/values-ast/strings.xml
@@ -1,5 +1,5 @@
- Aceutar y editar la suxerencia
+ Aceptar y editar la suxerencia
diff --git a/components/browser/awesomebar/src/main/res/values-skr/strings.xml b/components/browser/awesomebar/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..10bff0878f0
--- /dev/null
+++ b/components/browser/awesomebar/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+
+
+
+ تجویز کوں قبول کرو تے تبدیلی کرو
+
diff --git a/components/browser/domains/build.gradle b/components/browser/domains/build.gradle
index 438782bca9f..65bb01bf105 100644
--- a/components/browser/domains/build.gradle
+++ b/components/browser/domains/build.gradle
@@ -31,6 +31,7 @@ dependencies {
testImplementation Dependencies.androidx_test_junit
testImplementation Dependencies.testing_robolectric
+ testImplementation Dependencies.testing_coroutines
}
apply from: '../../../publish.gradle'
diff --git a/components/browser/domains/src/test/java/mozilla/components/browser/domains/CustomDomainsTest.kt b/components/browser/domains/src/test/java/mozilla/components/browser/domains/CustomDomainsTest.kt
index 755ee9a3a1a..3f3ec083636 100644
--- a/components/browser/domains/src/test/java/mozilla/components/browser/domains/CustomDomainsTest.kt
+++ b/components/browser/domains/src/test/java/mozilla/components/browser/domains/CustomDomainsTest.kt
@@ -6,7 +6,7 @@ package mozilla.components.browser.domains
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
@@ -24,11 +24,10 @@ class CustomDomainsTest {
.apply()
}
+ @ExperimentalCoroutinesApi
@Test
fun customListIsEmptyByDefault() {
- val domains = runBlocking {
- CustomDomains.load(testContext)
- }
+ val domains = CustomDomains.load(testContext)
assertEquals(0, domains.size)
}
diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt
index ee56f031d08..7c9cd7ba65a 100644
--- a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt
+++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt
@@ -429,6 +429,12 @@ class GeckoEngineSession(
runtime.webExtensionController.setTabActive(geckoSession, active)
}
+ /**
+ * See [EngineSession.updateSessionPriority].
+ */
+ override fun updateSessionPriority(priority: SessionPriority) {
+ geckoSession.setPriorityHint(priority.id)
+ }
/**
* Purges the history for the session (back and forward history).
*/
diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/autofill/GeckoAutocompleteStorageDelegate.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/autofill/GeckoAutocompleteStorageDelegate.kt
index 66770616917..a1185497a87 100644
--- a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/autofill/GeckoAutocompleteStorageDelegate.kt
+++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/autofill/GeckoAutocompleteStorageDelegate.kt
@@ -8,6 +8,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
+import mozilla.components.browser.engine.gecko.ext.toCreditCardEntry
import mozilla.components.browser.engine.gecko.ext.toLoginEntry
import mozilla.components.concept.storage.CreditCard
import mozilla.components.concept.storage.CreditCardsAddressesStorageDelegate
@@ -31,15 +32,17 @@ class GeckoAutocompleteStorageDelegate(
private val loginStorageDelegate: LoginStorageDelegate
) : Autocomplete.StorageDelegate {
- override fun onCreditCardFetch(): GeckoResult>? {
+ override fun onCreditCardFetch(): GeckoResult> {
val result = GeckoResult>()
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(IO) {
- val creditCards = creditCardsAddressesStorageDelegate.onCreditCardsFetch().await()
+ val key = creditCardsAddressesStorageDelegate.getOrGenerateKey()
+
+ val creditCards = creditCardsAddressesStorageDelegate.onCreditCardsFetch()
.mapNotNull {
val plaintextCardNumber =
- creditCardsAddressesStorageDelegate.decrypt(it.encryptedCardNumber)?.number
+ creditCardsAddressesStorageDelegate.decrypt(key, it.encryptedCardNumber)?.number
if (plaintextCardNumber == null) {
null
@@ -54,17 +57,25 @@ class GeckoAutocompleteStorageDelegate(
}
}
.toTypedArray()
+
result.complete(creditCards)
}
return result
}
+ override fun onCreditCardSave(creditCard: Autocomplete.CreditCard) {
+ @OptIn(DelicateCoroutinesApi::class)
+ GlobalScope.launch(IO) {
+ creditCardsAddressesStorageDelegate.onCreditCardSave(creditCard.toCreditCardEntry())
+ }
+ }
+
override fun onLoginSave(login: Autocomplete.LoginEntry) {
loginStorageDelegate.onLoginSave(login.toLoginEntry())
}
- override fun onLoginFetch(domain: String): GeckoResult>? {
+ override fun onLoginFetch(domain: String): GeckoResult> {
val result = GeckoResult>()
@OptIn(DelicateCoroutinesApi::class)
diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Address.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Address.kt
new file mode 100644
index 00000000000..06cd9bb0027
--- /dev/null
+++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Address.kt
@@ -0,0 +1,46 @@
+/* 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.engine.gecko.ext
+
+import mozilla.components.concept.storage.Address
+import org.mozilla.geckoview.Autocomplete
+
+/**
+ * Converts a GeckoView [Autocomplete.Address] to an Android Components [Address].
+ */
+fun Autocomplete.Address.toAddress() = Address(
+ guid = guid ?: "",
+ givenName = givenName,
+ additionalName = additionalName,
+ familyName = familyName,
+ organization = organization,
+ streetAddress = streetAddress,
+ addressLevel3 = addressLevel3,
+ addressLevel2 = addressLevel2,
+ addressLevel1 = addressLevel1,
+ postalCode = postalCode,
+ country = country,
+ tel = tel,
+ email = email
+)
+
+/**
+ * Converts an Android Components [Address] to a GeckoView [Autocomplete.Address].
+ */
+fun Address.toAutocompleteAddress() = Autocomplete.Address.Builder()
+ .guid(guid)
+ .givenName(givenName)
+ .additionalName(additionalName)
+ .familyName(familyName)
+ .organization(organization)
+ .streetAddress(streetAddress)
+ .addressLevel3(addressLevel3)
+ .addressLevel2(addressLevel2)
+ .addressLevel1(addressLevel1)
+ .postalCode(postalCode)
+ .country(country)
+ .tel(tel)
+ .email(email)
+ .build()
diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/CreditCard.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/CreditCard.kt
index b3b55760b8f..3706281d2dc 100644
--- a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/CreditCard.kt
+++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/CreditCard.kt
@@ -4,14 +4,14 @@
package mozilla.components.browser.engine.gecko.ext
-import mozilla.components.concept.engine.prompt.CreditCard
+import mozilla.components.concept.storage.CreditCardEntry
import mozilla.components.support.utils.creditCardIIN
import org.mozilla.geckoview.Autocomplete
/**
- * Converts a GeckoView [Autocomplete.CreditCard] to an Android Components [CreditCard].
+ * Converts a GeckoView [Autocomplete.CreditCard] to an Android Components [CreditCardEntry].
*/
-fun Autocomplete.CreditCard.toCreditCard() = CreditCard(
+fun Autocomplete.CreditCard.toCreditCardEntry() = CreditCardEntry(
guid = guid,
name = name,
number = number,
@@ -21,9 +21,9 @@ fun Autocomplete.CreditCard.toCreditCard() = CreditCard(
)
/**
- * Converts an Android Components [CreditCard] to a GeckoView [Autocomplete.CreditCard].
+ * Converts an Android Components [CreditCardEntry] to a GeckoView [Autocomplete.CreditCard].
*/
-fun CreditCard.toAutocompleteCreditCard() = Autocomplete.CreditCard.Builder()
+fun CreditCardEntry.toAutocompleteCreditCard() = Autocomplete.CreditCard.Builder()
.guid(guid)
.name(name)
.number(number)
diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/GeckoChoice.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/GeckoChoice.kt
new file mode 100644
index 00000000000..9b9c5e0726c
--- /dev/null
+++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/GeckoChoice.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.engine.gecko.ext
+
+import mozilla.components.browser.engine.gecko.prompt.GeckoChoice
+import mozilla.components.concept.engine.prompt.Choice
+
+/**
+ * Converts a GeckoView [GeckoChoice] to an Android Components [Choice].
+ */
+private fun GeckoChoice.toChoice(): Choice {
+ val choiceChildren = items?.map { it.toChoice() }?.toTypedArray()
+ // On the GeckoView docs states that label is a @NonNull, but on run-time
+ // we are getting null values
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1771149
+ @Suppress("USELESS_ELVIS")
+ return Choice(id, !disabled, label ?: "", selected, separator, choiceChildren)
+}
+
+/**
+ * Convert an array of [GeckoChoice] to Choice array.
+ * @return array of Choice
+ */
+fun convertToChoices(
+ geckoChoices: Array
+): Array = geckoChoices.map { geckoChoice ->
+ val choice = geckoChoice.toChoice()
+ choice
+}.toTypedArray()
diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/profiler/Profiler.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/profiler/Profiler.kt
index 98fd12f2037..437d7108b32 100644
--- a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/profiler/Profiler.kt
+++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/profiler/Profiler.kt
@@ -5,6 +5,7 @@
package mozilla.components.browser.engine.gecko.profiler
import mozilla.components.concept.base.profiler.Profiler
+import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoRuntime
/**
@@ -63,4 +64,21 @@ class Profiler(
override fun addMarker(markerName: String) {
runtime.profilerController.addMarker(markerName)
}
+
+ override fun startProfiler(filters: Array, features: Array) {
+ runtime.profilerController.startProfiler(filters, features)
+ }
+
+ override fun stopProfiler(onSuccess: (ByteArray?) -> Unit, onError: (Throwable) -> Unit) {
+ runtime.profilerController.stopProfiler().then(
+ { profileResult ->
+ onSuccess(profileResult)
+ GeckoResult()
+ },
+ { throwable ->
+ onError(throwable)
+ GeckoResult()
+ }
+ )
+ }
}
diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptUpdateDelegate.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptUpdateDelegate.kt
new file mode 100644
index 00000000000..2917c475398
--- /dev/null
+++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptUpdateDelegate.kt
@@ -0,0 +1,60 @@
+/* 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.engine.gecko.prompt
+
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.browser.engine.gecko.ext.convertToChoices
+import mozilla.components.concept.engine.prompt.PromptRequest
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.BasePrompt
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.ChoicePrompt
+import org.mozilla.geckoview.GeckoSession.PromptDelegate.PromptInstanceDelegate
+
+/**
+ * Implementation of [PromptInstanceDelegate] used to update a
+ * prompt request when onPromptUpdate is invoked.
+ *
+ * @param geckoSession [GeckoEngineSession] used to notify the engine observer
+ * with the onPromptUpdate callback.
+ * @param previousPrompt [PromptRequest] to be updated.
+ */
+internal class ChoicePromptUpdateDelegate(
+ private val geckoSession: GeckoEngineSession,
+ private var previousPrompt: PromptRequest,
+) : PromptInstanceDelegate {
+
+ override fun onPromptUpdate(prompt: BasePrompt) {
+ if (prompt is ChoicePrompt) {
+ val promptRequest = updatePromptChoices(prompt)
+ if (promptRequest != null) {
+ geckoSession.notifyObservers {
+ this.onPromptUpdate(previousPrompt.uid, promptRequest)
+ }
+ previousPrompt = promptRequest
+ }
+ }
+ }
+
+ /**
+ * Use the received prompt to create the updated [PromptRequest]
+ * @param updatedPrompt The [ChoicePrompt] with the updated choices.
+ */
+ private fun updatePromptChoices(updatedPrompt: ChoicePrompt): PromptRequest? {
+ return when (previousPrompt) {
+ is PromptRequest.MenuChoice -> {
+ (previousPrompt as PromptRequest.MenuChoice)
+ .copy(choices = convertToChoices(updatedPrompt.choices))
+ }
+ is PromptRequest.SingleChoice -> {
+ (previousPrompt as PromptRequest.SingleChoice)
+ .copy(choices = convertToChoices(updatedPrompt.choices))
+ }
+ is PromptRequest.MultipleChoice -> {
+ (previousPrompt as PromptRequest.MultipleChoice)
+ .copy(choices = convertToChoices(updatedPrompt.choices))
+ }
+ else -> null
+ }
+ }
+}
diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt
index 877723a49b3..f5f6fcb61ea 100644
--- a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt
+++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt
@@ -8,16 +8,20 @@ import android.content.Context
import android.net.Uri
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.browser.engine.gecko.ext.convertToChoices
+import mozilla.components.browser.engine.gecko.ext.toAddress
+import mozilla.components.browser.engine.gecko.ext.toAutocompleteAddress
import mozilla.components.browser.engine.gecko.ext.toAutocompleteCreditCard
-import mozilla.components.browser.engine.gecko.ext.toCreditCard
+import mozilla.components.browser.engine.gecko.ext.toCreditCardEntry
import mozilla.components.browser.engine.gecko.ext.toLoginEntry
import mozilla.components.concept.engine.prompt.Choice
-import mozilla.components.concept.engine.prompt.CreditCard
import mozilla.components.concept.engine.prompt.PromptRequest
import mozilla.components.concept.engine.prompt.PromptRequest.MenuChoice
import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice
import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
import mozilla.components.concept.engine.prompt.ShareData
+import mozilla.components.concept.storage.Address
+import mozilla.components.concept.storage.CreditCardEntry
import mozilla.components.concept.storage.Login
import mozilla.components.concept.storage.LoginEntry
import mozilla.components.support.base.log.logger.Logger
@@ -64,6 +68,43 @@ typealias AC_FILE_FACING_MODE = PromptRequest.File.FacingMode
internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSession) :
PromptDelegate {
+ override fun onCreditCardSave(
+ session: GeckoSession,
+ request: AutocompleteRequest
+ ): GeckoResult {
+ val geckoResult = GeckoResult()
+
+ val onConfirm: (CreditCardEntry) -> Unit = { creditCard ->
+ if (!request.isComplete) {
+ geckoResult.complete(
+ request.confirm(
+ Autocomplete.CreditCardSelectOption(creditCard.toAutocompleteCreditCard())
+ )
+ )
+ }
+ }
+
+ val onDismiss: () -> Unit = {
+ request.dismissSafely(geckoResult)
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.SaveCreditCard(
+ creditCard = request.options[0].value.toCreditCardEntry(),
+ onConfirm = onConfirm,
+ onDismiss = onDismiss
+ ).also {
+ request.delegate = PromptInstanceDismissDelegate(
+ geckoEngineSession, it
+ )
+ }
+ )
+ }
+
+ return geckoResult
+ }
+
/**
* Handle a credit card selection prompt request. This is triggered by the user
* focusing on a credit card input field.
@@ -77,7 +118,7 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe
): GeckoResult? {
val geckoResult = GeckoResult()
- val onConfirm: (CreditCard) -> Unit = { creditCard ->
+ val onConfirm: (CreditCardEntry) -> Unit = { creditCard ->
if (!request.isComplete) {
geckoResult.complete(
request.confirm(
@@ -94,7 +135,7 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe
geckoEngineSession.notifyObservers {
onPromptRequest(
PromptRequest.SelectCreditCard(
- creditCards = request.options.map { it.value.toCreditCard() },
+ creditCards = request.options.map { it.value.toCreditCardEntry() },
onDismiss = onDismiss,
onConfirm = onConfirm
)
@@ -217,6 +258,11 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe
else -> throw InvalidParameterException("${geckoPrompt.type} is not a valid Gecko @Choice.ChoiceType")
}
+ geckoPrompt.delegate = ChoicePromptUpdateDelegate(
+ geckoEngineSession,
+ promptRequest
+ )
+
geckoEngineSession.notifyObservers {
onPromptRequest(promptRequest)
}
@@ -224,6 +270,39 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe
return geckoResult
}
+ override fun onAddressSelect(
+ session: GeckoSession,
+ request: AutocompleteRequest
+ ): GeckoResult {
+ val geckoResult = GeckoResult()
+
+ val onConfirm: (Address) -> Unit = { address ->
+ if (!request.isComplete) {
+ geckoResult.complete(
+ request.confirm(
+ Autocomplete.AddressSelectOption(address.toAutocompleteAddress())
+ )
+ )
+ }
+ }
+
+ val onDismiss: () -> Unit = {
+ request.dismissSafely(geckoResult)
+ }
+
+ geckoEngineSession.notifyObservers {
+ onPromptRequest(
+ PromptRequest.SelectAddress(
+ addresses = request.options.map { it.value.toAddress() },
+ onConfirm = onConfirm,
+ onDismiss = onDismiss
+ )
+ )
+ }
+
+ return geckoResult
+ }
+
override fun onAlertPrompt(
session: GeckoSession,
prompt: PromptDelegate.AlertPrompt
@@ -600,28 +679,6 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe
return geckoResult
}
- private fun GeckoChoice.toChoice(): Choice {
- val choiceChildren = items?.map { it.toChoice() }?.toTypedArray()
- // On the GeckoView docs states that label is a @NonNull, but on run-time
- // we are getting null values
- @Suppress("USELESS_ELVIS")
- return Choice(id, !disabled, label ?: "", selected, separator, choiceChildren)
- }
-
- /**
- * Convert an array of [GeckoChoice] to Choice array.
- * @return array of Choice
- */
- private fun convertToChoices(
- geckoChoices: Array
- ): Array {
-
- return geckoChoices.map { geckoChoice ->
- val choice = geckoChoice.toChoice()
- choice
- }.toTypedArray()
- }
-
@Suppress("LongParameterList")
private fun notifyDatePromptRequest(
title: String,
@@ -721,7 +778,6 @@ internal fun Date.toString(format: String): String {
* Only dismiss if the prompt is not already dismissed.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-
internal fun PromptDelegate.BasePrompt.dismissSafely(geckoResult: GeckoResult) {
if (!this.isComplete) {
geckoResult.complete(dismiss())
diff --git a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionTest.kt b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionTest.kt
index 06098fffb89..e7c43a3c990 100644
--- a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionTest.kt
+++ b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionTest.kt
@@ -12,7 +12,6 @@ import android.os.Message
import android.view.WindowManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.engine.gecko.ext.geckoTrackingProtectionPermission
import mozilla.components.browser.engine.gecko.ext.isExcludedForTrackingProtection
import mozilla.components.browser.engine.gecko.permission.geckoContentPermission
@@ -44,6 +43,8 @@ import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.eq
import mozilla.components.support.test.expectException
import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import mozilla.components.support.test.whenever
import mozilla.components.support.utils.ThreadUtils
import mozilla.components.test.ReflectionUtils
@@ -55,6 +56,7 @@ import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
@@ -102,6 +104,9 @@ typealias GeckoCookieBehavior = ContentBlocking.CookieBehavior
@RunWith(AndroidJUnit4::class)
class GeckoEngineSessionTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
private lateinit var runtime: GeckoRuntime
private lateinit var geckoSession: GeckoSession
private lateinit var geckoSessionProvider: () -> GeckoSession
@@ -774,7 +779,7 @@ class GeckoEngineSessionTest {
}
@Test
- fun `notifies configured history delegate of title changes`() = runBlockingTest {
+ fun `notifies configured history delegate of title changes`() = runTestOnMain {
val engineSession = GeckoEngineSession(
runtime, geckoSessionProvider = geckoSessionProvider,
context = coroutineContext
@@ -801,7 +806,7 @@ class GeckoEngineSessionTest {
}
@Test
- fun `does not notify configured history delegate of title changes for private sessions`() = runBlockingTest {
+ fun `does not notify configured history delegate of title changes for private sessions`() = runTestOnMain {
val engineSession = GeckoEngineSession(
mock(),
geckoSessionProvider = geckoSessionProvider,
@@ -833,7 +838,7 @@ class GeckoEngineSessionTest {
}
@Test
- fun `notifies configured history delegate of preview image URL changes`() = runBlockingTest {
+ fun `notifies configured history delegate of preview image URL changes`() = runTestOnMain {
val engineSession = GeckoEngineSession(
runtime, geckoSessionProvider = geckoSessionProvider,
context = coroutineContext
@@ -864,7 +869,7 @@ class GeckoEngineSessionTest {
}
@Test
- fun `does not notify configured history delegate of preview image URL changes for private sessions`() = runBlockingTest {
+ fun `does not notify configured history delegate of preview image URL changes for private sessions`() = runTestOnMain {
val engineSession = GeckoEngineSession(
mock(),
geckoSessionProvider = geckoSessionProvider,
@@ -896,7 +901,7 @@ class GeckoEngineSessionTest {
}
@Test
- fun `does not notify configured history delegate for top-level visits to error pages`() = runBlockingTest {
+ fun `does not notify configured history delegate for top-level visits to error pages`() = runTestOnMain {
val engineSession = GeckoEngineSession(
mock(),
geckoSessionProvider = geckoSessionProvider,
@@ -919,7 +924,7 @@ class GeckoEngineSessionTest {
}
@Test
- fun `notifies configured history delegate of visits`() = runBlockingTest {
+ fun `notifies configured history delegate of visits`() = runTestOnMain {
val engineSession = GeckoEngineSession(
mock(),
geckoSessionProvider = geckoSessionProvider,
@@ -938,7 +943,7 @@ class GeckoEngineSessionTest {
}
@Test
- fun `notifies configured history delegate of reloads`() = runBlockingTest {
+ fun `notifies configured history delegate of reloads`() = runTestOnMain {
val engineSession = GeckoEngineSession(
mock(),
geckoSessionProvider = geckoSessionProvider,
@@ -957,7 +962,7 @@ class GeckoEngineSessionTest {
}
@Test
- fun `checks with the delegate before trying to record a visit`() = runBlockingTest {
+ fun `checks with the delegate before trying to record a visit`() = runTestOnMain {
val engineSession = GeckoEngineSession(
mock(),
geckoSessionProvider = geckoSessionProvider,
@@ -985,7 +990,7 @@ class GeckoEngineSessionTest {
}
@Test
- fun `correctly processes redirect visit flags`() = runBlockingTest {
+ fun `correctly processes redirect visit flags`() = runTestOnMain {
val engineSession = GeckoEngineSession(
mock(),
geckoSessionProvider = geckoSessionProvider,
@@ -1047,7 +1052,7 @@ class GeckoEngineSessionTest {
}
@Test
- fun `does not notify configured history delegate of visits for private sessions`() = runBlockingTest {
+ fun `does not notify configured history delegate of visits for private sessions`() = runTestOnMain {
val engineSession = GeckoEngineSession(
mock(),
geckoSessionProvider = geckoSessionProvider,
@@ -1066,7 +1071,7 @@ class GeckoEngineSessionTest {
}
@Test
- fun `requests visited URLs from configured history delegate`() = runBlockingTest {
+ fun `requests visited URLs from configured history delegate`() = runTestOnMain {
val engineSession = GeckoEngineSession(
mock(),
geckoSessionProvider = geckoSessionProvider,
@@ -1088,7 +1093,7 @@ class GeckoEngineSessionTest {
}
@Test
- fun `does not request visited URLs from configured history delegate in private sessions`() = runBlockingTest {
+ fun `does not request visited URLs from configured history delegate in private sessions`() = runTestOnMain {
val engineSession = GeckoEngineSession(
mock(),
geckoSessionProvider = geckoSessionProvider,
@@ -1107,7 +1112,7 @@ class GeckoEngineSessionTest {
}
@Test
- fun `notifies configured history delegate of state changes`() = runBlockingTest {
+ fun `notifies configured history delegate of state changes`() = runTestOnMain {
val engineSession = GeckoEngineSession(
mock(),
geckoSessionProvider = geckoSessionProvider,
diff --git a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoResultTest.kt b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoResultTest.kt
index 27e47bda766..cd56ce249aa 100644
--- a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoResultTest.kt
+++ b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoResultTest.kt
@@ -6,7 +6,7 @@ package mozilla.components.browser.engine.gecko
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.support.test.mock
import mozilla.components.support.test.whenever
import org.junit.Assert.assertEquals
@@ -25,18 +25,18 @@ import org.robolectric.annotation.LooperMode
class GeckoResultTest {
@Test
- fun awaitWithResult() = runBlockingTest {
+ fun awaitWithResult() = runTest {
val result = GeckoResult.fromValue(42).await()
assertEquals(42, result)
}
@Test(expected = IllegalStateException::class)
- fun awaitWithException() = runBlockingTest {
+ fun awaitWithException() = runTest {
GeckoResult.fromException(IllegalStateException()).await()
}
@Test
- fun fromResult() = runBlockingTest {
+ fun fromResult() = runTest {
val result = launchGeckoResult { 42 }
result.then {
@@ -46,7 +46,7 @@ class GeckoResultTest {
}
@Test
- fun fromException() = runBlockingTest {
+ fun fromException() = runTest {
val result = launchGeckoResult { throw IllegalStateException() }
result.then(
@@ -62,7 +62,7 @@ class GeckoResultTest {
}
@Test
- fun asCancellableOperation() = runBlockingTest {
+ fun asCancellableOperation() = runTest {
val geckoResult: GeckoResult = mock()
val op = geckoResult.asCancellableOperation()
diff --git a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorageTest.kt b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorageTest.kt
index 2d87ec73602..d31deb2adc5 100644
--- a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorageTest.kt
+++ b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorageTest.kt
@@ -6,7 +6,7 @@ package mozilla.components.browser.engine.gecko.permission
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus
import mozilla.components.concept.engine.permission.SitePermissions.Status.ALLOWED
@@ -66,7 +66,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `GIVEN a location permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runBlockingTest {
+ fun `GIVEN a location permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runTest {
val sitePermissions = createNewSitePermission().copy(location = ALLOWED)
val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_GEOLOCATION)
val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_GEOLOCATION, geckoPermissions, mock())
@@ -83,7 +83,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `GIVEN a notification permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runBlockingTest {
+ fun `GIVEN a notification permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runTest {
val sitePermissions = createNewSitePermission().copy(notification = BLOCKED)
val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_DESKTOP_NOTIFICATION)
val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_DESKTOP_NOTIFICATION, geckoPermissions, mock())
@@ -100,7 +100,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `GIVEN a localStorage permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runBlockingTest {
+ fun `GIVEN a localStorage permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runTest {
val sitePermissions = createNewSitePermission().copy(localStorage = BLOCKED)
val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_PERSISTENT_STORAGE)
val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_PERSISTENT_STORAGE, geckoPermissions, mock())
@@ -117,7 +117,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `GIVEN a crossOriginStorageAccess permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runBlockingTest {
+ fun `GIVEN a crossOriginStorageAccess permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runTest {
val sitePermissions = createNewSitePermission().copy(crossOriginStorageAccess = BLOCKED)
val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_STORAGE_ACCESS)
val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_STORAGE_ACCESS, geckoPermissions, mock())
@@ -134,7 +134,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `GIVEN a mediaKeySystemAccess permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runBlockingTest {
+ fun `GIVEN a mediaKeySystemAccess permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runTest {
val sitePermissions = createNewSitePermission().copy(mediaKeySystemAccess = ALLOWED)
val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_MEDIA_KEY_SYSTEM_ACCESS)
val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_MEDIA_KEY_SYSTEM_ACCESS, geckoPermissions, mock())
@@ -151,7 +151,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `GIVEN a autoplayInaudible permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runBlockingTest {
+ fun `GIVEN a autoplayInaudible permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runTest {
val sitePermissions = createNewSitePermission().copy(autoplayInaudible = AutoplayStatus.ALLOWED)
val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_AUTOPLAY_INAUDIBLE)
val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_AUTOPLAY_INAUDIBLE, geckoPermissions, mock())
@@ -168,7 +168,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `GIVEN a autoplayAudible permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runBlockingTest {
+ fun `GIVEN a autoplayAudible permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runTest {
val sitePermissions = createNewSitePermission().copy(autoplayAudible = AutoplayStatus.ALLOWED)
val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE)
val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE, geckoPermissions, mock())
@@ -185,7 +185,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `WHEN saving a site permission THEN the permission is saved in the gecko storage and in disk storage`() = runBlockingTest {
+ fun `WHEN saving a site permission THEN the permission is saved in the gecko storage and in disk storage`() = runTest {
val sitePermissions = createNewSitePermission().copy(autoplayAudible = AutoplayStatus.ALLOWED)
val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE)
val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE, geckoPermissions, mock())
@@ -199,7 +199,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `GIVEN a temporary permission WHEN saving THEN the permission is saved in memory`() = runBlockingTest {
+ fun `GIVEN a temporary permission WHEN saving THEN the permission is saved in memory`() = runTest {
val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE)
val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE, geckoPermissions, mock())
@@ -209,7 +209,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `GIVEN media type temporary permission WHEN saving THEN the permission is NOT saved in memory`() = runBlockingTest {
+ fun `GIVEN media type temporary permission WHEN saving THEN the permission is NOT saved in memory`() = runTest {
val geckoRequest = GeckoPermissionRequest.Media("mozilla.org", emptyList(), emptyList(), mock())
assertTrue(geckoStorage.geckoTemporaryPermissions.isEmpty())
@@ -220,7 +220,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `GIVEN multiple saved temporary permissions WHEN clearing all temporary permission THEN all permissions are cleared`() = runBlockingTest {
+ fun `GIVEN multiple saved temporary permissions WHEN clearing all temporary permission THEN all permissions are cleared`() = runTest {
val geckoAutoPlayPermissions = geckoContentPermission("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE)
val geckoPersistentStoragePermissions = geckoContentPermission("mozilla.org", PERMISSION_PERSISTENT_STORAGE)
val geckoStorageAccessPermissions = geckoContentPermission("mozilla.org", PERMISSION_STORAGE_ACCESS)
@@ -250,7 +250,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `GIVEN a localStorage permission WHEN updating THEN the permission is updated in the gecko storage and set to the default value on the disk storage`() = runBlockingTest {
+ fun `GIVEN a localStorage permission WHEN updating THEN the permission is updated in the gecko storage and set to the default value on the disk storage`() = runTest {
val sitePermissions = createNewSitePermission().copy(location = ALLOWED)
val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_GEOLOCATION)
val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE, geckoPermissions, mock())
@@ -264,7 +264,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `WHEN updating a permission THEN the permission is updated in the gecko storage and on the disk storage`() = runBlockingTest {
+ fun `WHEN updating a permission THEN the permission is updated in the gecko storage and on the disk storage`() = runTest {
val sitePermissions = createNewSitePermission().copy(location = ALLOWED)
doReturn(sitePermissions).`when`(geckoStorage).updateGeckoPermissionIfNeeded(sitePermissions)
@@ -276,7 +276,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `WHEN updating THEN the permission is updated in the gecko storage and set to the default value on the disk storage`() = runBlockingTest {
+ fun `WHEN updating THEN the permission is updated in the gecko storage and set to the default value on the disk storage`() = runTest {
val sitePermissions = SitePermissions(
origin = "mozilla.dev",
localStorage = ALLOWED,
@@ -323,7 +323,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `WHEN querying the store by origin THEN the gecko and the on disk storage are queried and results are combined`() = runBlockingTest {
+ fun `WHEN querying the store by origin THEN the gecko and the on disk storage are queried and results are combined`() = runTest {
val sitePermissions = SitePermissions(
origin = "mozilla.dev",
localStorage = ALLOWED,
@@ -365,7 +365,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `GIVEN a gecko and on disk permissions WHEN merging values THEN both should be combined into one`() = runBlockingTest {
+ fun `GIVEN a gecko and on disk permissions WHEN merging values THEN both should be combined into one`() = runTest {
val onDiskPermissions = SitePermissions(
origin = "mozilla.dev",
localStorage = ALLOWED,
@@ -404,7 +404,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `GIVEN permissions that are not present on the gecko storage WHEN merging THEN favor the values on disk permissions`() = runBlockingTest {
+ fun `GIVEN permissions that are not present on the gecko storage WHEN merging THEN favor the values on disk permissions`() = runTest {
val onDiskPermissions = SitePermissions(
origin = "mozilla.dev",
localStorage = ALLOWED,
@@ -471,7 +471,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `WHEN removing a site permissions THEN permissions should be removed from the on disk and gecko storage`() = runBlockingTest {
+ fun `WHEN removing a site permissions THEN permissions should be removed from the on disk and gecko storage`() = runTest {
val onDiskPermissions = createNewSitePermission()
doReturn(Unit).`when`(geckoStorage).removeGeckoContentPermissionBy(onDiskPermissions.origin)
@@ -483,7 +483,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `WHEN removing gecko permissions THEN permissions should be set to the default values in the gecko storage`() = runBlockingTest {
+ fun `WHEN removing gecko permissions THEN permissions should be set to the default values in the gecko storage`() = runTest {
val geckoPermissions = listOf(
geckoContentPermission(type = PERMISSION_GEOLOCATION),
geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION),
@@ -511,7 +511,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `WHEN removing a temporary permissions THEN the permissions should be remove from memory`() = runBlockingTest {
+ fun `WHEN removing a temporary permissions THEN the permissions should be remove from memory`() = runTest {
val geckoPermissions = listOf(
geckoContentPermission(type = PERMISSION_GEOLOCATION),
geckoContentPermission(type = PERMISSION_GEOLOCATION),
@@ -539,7 +539,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `WHEN removing all THEN all permissions should be removed from the on disk and gecko storage`() = runBlockingTest {
+ fun `WHEN removing all THEN all permissions should be removed from the on disk and gecko storage`() = runTest {
doReturn(Unit).`when`(geckoStorage).removeGeckoAllContentPermissions()
@@ -550,7 +550,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `WHEN removing all gecko permissions THEN remove all permissions on gecko and clear the site permissions info`() = runBlockingTest {
+ fun `WHEN removing all gecko permissions THEN remove all permissions on gecko and clear the site permissions info`() = runTest {
val geckoPermissions = listOf(
geckoContentPermission(type = PERMISSION_GEOLOCATION),
geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION),
@@ -574,7 +574,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `WHEN querying all permission THEN the gecko and the on disk storage are queried and results are combined`() = runBlockingTest {
+ fun `WHEN querying all permission THEN the gecko and the on disk storage are queried and results are combined`() = runTest {
val onDiskPermissions = SitePermissions(
origin = "mozilla.dev",
localStorage = ALLOWED,
@@ -616,7 +616,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `WHEN filtering temporary permissions THEN all temporary permissions should be removed`() = runBlockingTest {
+ fun `WHEN filtering temporary permissions THEN all temporary permissions should be removed`() = runTest {
val temporary = listOf(geckoContentPermission(type = PERMISSION_GEOLOCATION))
val geckoPermissions = listOf(
@@ -637,7 +637,7 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `WHEN compering two gecko ContentPermissions THEN they are the same when host and permissions are the same`() = runBlockingTest {
+ fun `WHEN compering two gecko ContentPermissions THEN they are the same when host and permissions are the same`() = runTest {
val location1 = geckoContentPermission(uri = "mozilla.dev", type = PERMISSION_GEOLOCATION)
val location2 = geckoContentPermission(uri = "mozilla.dev", type = PERMISSION_GEOLOCATION)
val notification = geckoContentPermission(uri = "mozilla.dev", type = PERMISSION_DESKTOP_NOTIFICATION)
@@ -647,28 +647,28 @@ class GeckoSitePermissionsStorageTest {
}
@Test
- fun `WHEN converting from gecko status to sitePermissions status THEN they get converted to the equivalent one`() = runBlockingTest {
+ fun `WHEN converting from gecko status to sitePermissions status THEN they get converted to the equivalent one`() = runTest {
assertEquals(NO_DECISION, VALUE_PROMPT.toStatus())
assertEquals(BLOCKED, VALUE_DENY.toStatus())
assertEquals(ALLOWED, VALUE_ALLOW.toStatus())
}
@Test
- fun `WHEN converting from gecko status to autoplay sitePermissions status THEN they get converted to the equivalent one`() = runBlockingTest {
+ fun `WHEN converting from gecko status to autoplay sitePermissions status THEN they get converted to the equivalent one`() = runTest {
assertEquals(AutoplayStatus.BLOCKED, VALUE_PROMPT.toAutoPlayStatus())
assertEquals(AutoplayStatus.BLOCKED, VALUE_DENY.toAutoPlayStatus())
assertEquals(AutoplayStatus.ALLOWED, VALUE_ALLOW.toAutoPlayStatus())
}
@Test
- fun `WHEN converting a sitePermissions status to gecko status THEN they get converted to the equivalent one`() = runBlockingTest {
+ fun `WHEN converting a sitePermissions status to gecko status THEN they get converted to the equivalent one`() = runTest {
assertEquals(VALUE_PROMPT, NO_DECISION.toGeckoStatus())
assertEquals(VALUE_DENY, BLOCKED.toGeckoStatus())
assertEquals(VALUE_ALLOW, ALLOWED.toGeckoStatus())
}
@Test
- fun `WHEN converting from autoplay sitePermissions to gecko status THEN they get converted to the equivalent one`() = runBlockingTest {
+ fun `WHEN converting from autoplay sitePermissions to gecko status THEN they get converted to the equivalent one`() = runTest {
assertEquals(VALUE_DENY, AutoplayStatus.BLOCKED.toGeckoStatus())
assertEquals(VALUE_ALLOW, AutoplayStatus.ALLOWED.toGeckoStatus())
}
diff --git a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptUpdateDelegateTest.kt b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptUpdateDelegateTest.kt
new file mode 100644
index 00000000000..f6ca2a42e3b
--- /dev/null
+++ b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptUpdateDelegateTest.kt
@@ -0,0 +1,58 @@
+/* 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.engine.gecko.prompt
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.support.test.mock
+import mozilla.components.test.ReflectionUtils
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.geckoview.GeckoSession
+
+@RunWith(AndroidJUnit4::class)
+class ChoicePromptUpdateDelegateTest {
+
+ @Test
+ fun `WHEN onPromptUpdate is called from GeckoView THEN notifyObservers is invoked with onPromptUpdate`() {
+ val mockSession = GeckoEngineSession(mock())
+ var isOnPromptUpdateCalled = false
+ var isOnConfirmCalled = false
+ var isOnDismissCalled = false
+ var observedPrompt: PromptRequest? = null
+ var observedUID: String? = null
+ mockSession.register(object : EngineSession.Observer {
+ override fun onPromptUpdate(
+ previousPromptRequestUid: String,
+ promptRequest: PromptRequest
+ ) {
+ observedPrompt = promptRequest
+ observedUID = previousPromptRequestUid
+ isOnPromptUpdateCalled = true
+ }
+ })
+ val prompt = PromptRequest.SingleChoice(
+ arrayOf(),
+ { isOnConfirmCalled = true },
+ { isOnDismissCalled = true }
+ )
+ val delegate = ChoicePromptUpdateDelegate(mockSession, prompt)
+ val updatedPrompt = mock()
+ ReflectionUtils.setField(updatedPrompt, "choices", arrayOf())
+
+ delegate.onPromptUpdate(updatedPrompt)
+
+ assertTrue(isOnPromptUpdateCalled)
+ assertEquals(prompt.uid, observedUID)
+ // Verify if the onConfirm and onDismiss callbacks were changed
+ (observedPrompt as PromptRequest.SingleChoice).onConfirm(mock())
+ (observedPrompt as PromptRequest.SingleChoice).onDismiss()
+ assertTrue(isOnDismissCalled)
+ assertTrue(isOnConfirmCalled)
+ }
+}
diff --git a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt
index 9a67b9ebc34..04ea9846f45 100644
--- a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt
+++ b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt
@@ -8,14 +8,16 @@ import android.net.Uri
import android.os.Looper.getMainLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.browser.engine.gecko.GeckoEngineSession
+import mozilla.components.browser.engine.gecko.ext.toAutocompleteAddress
import mozilla.components.browser.engine.gecko.ext.toAutocompleteCreditCard
import mozilla.components.browser.engine.gecko.ext.toLoginEntry
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.prompt.Choice
-import mozilla.components.concept.engine.prompt.CreditCard
import mozilla.components.concept.engine.prompt.PromptRequest
import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice
import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
+import mozilla.components.concept.storage.Address
+import mozilla.components.concept.storage.CreditCardEntry
import mozilla.components.concept.storage.Login
import mozilla.components.concept.storage.LoginEntry
import mozilla.components.support.ktx.kotlin.toDate
@@ -834,6 +836,95 @@ class GeckoPromptDelegateTest {
passwordField = passwordField
)
+ @Test
+ fun `Calling onCreditCardSave must provide an SaveCreditCard PromptRequest`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var onCreditCardSaved = false
+ var onDismissWasCalled = false
+
+ var saveCreditCardPrompt: PromptRequest.SaveCreditCard = mock()
+
+ val promptDelegate = spy(GeckoPromptDelegate(mockSession))
+
+ mockSession.register(object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ saveCreditCardPrompt = promptRequest as PromptRequest.SaveCreditCard
+ }
+ })
+
+ val creditCard = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "amex"
+ )
+ val creditCardSaveOption =
+ Autocomplete.CreditCardSaveOption(creditCard.toAutocompleteCreditCard())
+
+ var geckoResult = promptDelegate.onCreditCardSave(
+ mock(),
+ geckoCreditCardSavePrompt(arrayOf(creditCardSaveOption))
+ )
+
+ geckoResult.accept {
+ onDismissWasCalled = true
+ }
+
+ saveCreditCardPrompt.onDismiss()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(onDismissWasCalled)
+
+ val geckoPrompt = geckoCreditCardSavePrompt(arrayOf(creditCardSaveOption))
+ geckoResult = promptDelegate.onCreditCardSave(mock(), geckoPrompt)
+
+ geckoResult.accept {
+ onCreditCardSaved = true
+ }
+
+ saveCreditCardPrompt.onConfirm(creditCard)
+ shadowOf(getMainLooper()).idle()
+
+ assertTrue(onCreditCardSaved)
+
+ whenever(geckoPrompt.isComplete).thenReturn(true)
+ onCreditCardSaved = false
+ saveCreditCardPrompt.onConfirm(creditCard)
+
+ assertFalse(onCreditCardSaved)
+ }
+
+ @Test
+ fun `Calling onCreditSave must set a PromptInstanceDismissDelegate`() {
+ val mockSession = GeckoEngineSession(runtime)
+ var saveCreditCardPrompt: PromptRequest.SaveCreditCard = mock()
+ val promptDelegate = spy(GeckoPromptDelegate(mockSession))
+
+ mockSession.register(object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ saveCreditCardPrompt = promptRequest as PromptRequest.SaveCreditCard
+ }
+ })
+
+ val creditCard = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "amex"
+ )
+ val creditCardSaveOption =
+ Autocomplete.CreditCardSaveOption(creditCard.toAutocompleteCreditCard())
+ val geckoPrompt = geckoCreditCardSavePrompt(arrayOf(creditCardSaveOption))
+
+ promptDelegate.onCreditCardSave(mock(), geckoPrompt)
+
+ assertNotNull(saveCreditCardPrompt)
+ assertNotNull(geckoPrompt.delegate)
+ }
+
@Test
fun `Calling onCreditCardSelect must provide as CreditCardSelectOption PromptRequest`() {
val mockSession = GeckoEngineSession(runtime)
@@ -850,7 +941,7 @@ class GeckoPromptDelegateTest {
}
})
- val creditCard1 = CreditCard(
+ val creditCard1 = CreditCardEntry(
guid = "1",
name = "Banana Apple",
number = "4111111111111110",
@@ -861,7 +952,7 @@ class GeckoPromptDelegateTest {
val creditCardSelectOption1 =
Autocomplete.CreditCardSelectOption(creditCard1.toAutocompleteCreditCard())
- val creditCard2 = CreditCard(
+ val creditCard2 = CreditCardEntry(
guid = "2",
name = "Orange Pineapple",
number = "4111111111115555",
@@ -1436,6 +1527,96 @@ class GeckoPromptDelegateTest {
verify(geckoResult, never()).complete(any())
}
+ @Test
+ fun `WHEN onAddressSelect is called THEN SelectAddress prompt request must be provided with the correct callbacks`() {
+ val mockSession = GeckoEngineSession(runtime)
+
+ var isOnConfirmCalled = false
+ var isOnDismissCalled = false
+
+ var selectAddressPrompt: PromptRequest.SelectAddress = mock()
+
+ val promptDelegate = spy(GeckoPromptDelegate(mockSession))
+
+ // Capture the SelectAddress prompt request
+ mockSession.register(object : EngineSession.Observer {
+ override fun onPromptRequest(promptRequest: PromptRequest) {
+ selectAddressPrompt = promptRequest as PromptRequest.SelectAddress
+ }
+ })
+
+ val address = Address(
+ guid = "1",
+ givenName = "Firefox",
+ additionalName = "-",
+ familyName = "-",
+ organization = "-",
+ streetAddress = "street",
+ addressLevel3 = "address3",
+ addressLevel2 = "address2",
+ addressLevel1 = "address1",
+ postalCode = "1",
+ country = "Country",
+ tel = "1",
+ email = "@"
+ )
+ val addressSelectOption =
+ Autocomplete.AddressSelectOption(address.toAutocompleteAddress())
+
+ var geckoPrompt =
+ geckoSelectAddressPrompt(arrayOf(addressSelectOption))
+
+ var geckoResult = promptDelegate.onAddressSelect(
+ mock(),
+ geckoPrompt
+ )
+
+ // Verify that the onDismiss callback was called
+ geckoResult.accept {
+ isOnDismissCalled = true
+ }
+
+ selectAddressPrompt.onDismiss()
+ shadowOf(getMainLooper()).idle()
+ assertTrue(isOnDismissCalled)
+
+ // Verify that the onConfirm callback was called
+ geckoPrompt =
+ geckoSelectAddressPrompt(arrayOf(addressSelectOption))
+
+ geckoResult = promptDelegate.onAddressSelect(
+ mock(),
+ geckoPrompt
+ )
+
+ geckoResult.accept {
+ isOnConfirmCalled = true
+ }
+
+ selectAddressPrompt.onConfirm(selectAddressPrompt.addresses.first())
+ shadowOf(getMainLooper()).idle()
+ assertTrue(isOnConfirmCalled)
+
+ // Verify that when the prompt request is already completed and onConfirm callback is called,
+ // then onConfirm callback is not executed
+ isOnConfirmCalled = false
+ geckoPrompt =
+ geckoSelectAddressPrompt(arrayOf(addressSelectOption), true)
+
+ geckoResult = promptDelegate.onAddressSelect(
+ mock(),
+ geckoPrompt
+ )
+
+ geckoResult.accept {
+ isOnConfirmCalled = true
+ }
+
+ selectAddressPrompt.onConfirm(selectAddressPrompt.addresses.first())
+ shadowOf(getMainLooper()).idle()
+ assertFalse(isOnConfirmCalled)
+ }
+
private fun geckoChoicePrompt(
title: String,
message: String,
@@ -1595,4 +1776,28 @@ class GeckoPromptDelegateTest {
ReflectionUtils.setField(prompt, "options", creditCards)
return prompt
}
+
+ private fun geckoSelectAddressPrompt(
+ addresses: Array,
+ isComplete: Boolean = false
+ ): GeckoSession.PromptDelegate.AutocompleteRequest {
+ val prompt: GeckoSession.PromptDelegate.AutocompleteRequest =
+ mock()
+ whenever(prompt.isComplete).thenReturn(isComplete)
+ ReflectionUtils.setField(prompt, "options", addresses)
+ return prompt
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun geckoCreditCardSavePrompt(
+ creditCard: Array
+ ): GeckoSession.PromptDelegate.AutocompleteRequest {
+ val prompt = Mockito.mock(
+ GeckoSession.PromptDelegate.AutocompleteRequest::class.java,
+ Mockito.RETURNS_DEEP_STUBS // for testing prompt.delegate
+ ) as GeckoSession.PromptDelegate.AutocompleteRequest
+
+ ReflectionUtils.setField(prompt, "options", creditCard)
+ return prompt
+ }
}
diff --git a/components/browser/engine-system/build.gradle b/components/browser/engine-system/build.gradle
index 2092ff0212b..259e039b5bb 100644
--- a/components/browser/engine-system/build.gradle
+++ b/components/browser/engine-system/build.gradle
@@ -42,6 +42,7 @@ dependencies {
testImplementation Dependencies.androidx_test_junit
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
+ testImplementation Dependencies.testing_coroutines
androidTestImplementation Dependencies.androidx_test_core
androidTestImplementation Dependencies.androidx_test_runner
diff --git a/components/browser/engine-system/src/main/res/values-ast/strings.xml b/components/browser/engine-system/src/main/res/values-ast/strings.xml
index 02e8f021e81..3f503d35e7f 100644
--- a/components/browser/engine-system/src/main/res/values-ast/strings.xml
+++ b/components/browser/engine-system/src/main/res/values-ast/strings.xml
@@ -3,8 +3,8 @@
La páxina en %1$s diz:
- %2$s ta solicitando un nome d\'usuariu y una contraseña. El sitiu diz «%1$s»
+ %1$s will be replaced by the hostname or a description of the protected area/site, %2$s will be replaced with the URL of the current page (displaying the dialog). -->
+ «%2$s» solicita un nome d\'usuariu y una contraseña. El sitiu diz «%1$s»
- %1$s ta solicitando un nome d\'usuariu y una contraseña.
+ «%1$s» solicita un nome d\'usuariu y una contraseña.
diff --git a/components/browser/engine-system/src/main/res/values-skr/strings.xml b/components/browser/engine-system/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..f5b21bf6997
--- /dev/null
+++ b/components/browser/engine-system/src/main/res/values-skr/strings.xml
@@ -0,0 +1,10 @@
+
+
+
+ %1$s تے ورقہ آہدے:
+
+ %2$s ورتݨ ناں تے پاسورڈ دی ارداس کریندا پئے۔ سائٹ آہدی ہے: “%1$s”
+
+ %1$s تہاݙے ورتݨ ناں تے پاسورڈ دی ارداس کریندا پئے۔
+
diff --git a/components/browser/engine-system/src/main/res/values-yo/strings.xml b/components/browser/engine-system/src/main/res/values-yo/strings.xml
index 7c391c1b022..c46a5432ec3 100644
--- a/components/browser/engine-system/src/main/res/values-yo/strings.xml
+++ b/components/browser/engine-system/src/main/res/values-yo/strings.xml
@@ -1,8 +1,10 @@
- Ojú-ìlà %1$s sọ pé:
+ Ojú-ìwé %1$s sọ pé:
- %2$s ń béèrè orúkọ aṣàmúlò àti kóòdù rẹ. Ìkànnì náà sọ pé: “%1$s”
-
+ %2$s ń béèrè orúkọ ìṣàmúlò àti kóòdù rẹ. Ìkànnì náà sọ pé: “%1$s”
+
+ %1$s is requesting your username and password.
+
diff --git a/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineSessionTest.kt b/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineSessionTest.kt
index 5e8c1a95d67..09af18f091c 100644
--- a/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineSessionTest.kt
+++ b/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineSessionTest.kt
@@ -15,7 +15,8 @@ import android.webkit.WebView
import android.webkit.WebViewClient
import android.webkit.WebViewDatabase
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.engine.system.matcher.UrlMatcher
import mozilla.components.browser.errorpages.ErrorType
import mozilla.components.concept.engine.DefaultSettings
@@ -236,8 +237,9 @@ class SystemEngineSessionTest {
verify(webView).restoreState(bundle)
}
+ @ExperimentalCoroutinesApi
@Test
- fun enableTrackingProtection() {
+ fun enableTrackingProtection() = runTest {
SystemEngineView.URL_MATCHER = UrlMatcher(arrayOf(""))
val engineSession = spy(SystemEngineSession(testContext))
@@ -257,7 +259,7 @@ class SystemEngineSessionTest {
})
assertNull(engineSession.trackingProtectionPolicy)
- runBlocking { engineSession.updateTrackingProtection() }
+ engineSession.updateTrackingProtection()
assertEquals(
EngineSession.TrackingProtectionPolicy.strict(),
engineSession.trackingProtectionPolicy
diff --git a/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineViewTest.kt b/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineViewTest.kt
index 415c3cb10e6..b9b6e2330be 100644
--- a/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineViewTest.kt
+++ b/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineViewTest.kt
@@ -30,7 +30,8 @@ import android.webkit.WebViewClient
import android.webkit.WebViewDatabase
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.engine.system.matcher.UrlMatcher
import mozilla.components.browser.errorpages.ErrorType
import mozilla.components.concept.engine.EngineSession
@@ -77,6 +78,7 @@ import org.robolectric.Robolectric
import org.robolectric.annotation.Config
import java.io.StringReader
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class SystemEngineViewTest {
@@ -285,7 +287,7 @@ class SystemEngineViewTest {
}
@Test
- fun `WebView client notifies configured history delegate of url visits`() = runBlocking {
+ fun `WebView client notifies configured history delegate of url visits`() = runTest {
val engineSession = SystemEngineSession(testContext)
val engineView = SystemEngineView(testContext)
@@ -308,7 +310,7 @@ class SystemEngineViewTest {
}
@Test
- fun `WebView client checks with the delegate if the URI visit should be recorded`() = runBlocking {
+ fun `WebView client checks with the delegate if the URI visit should be recorded`() = runTest {
val engineSession = SystemEngineSession(testContext)
val engineView = SystemEngineView(testContext)
val webView: WebView = mock()
@@ -332,7 +334,7 @@ class SystemEngineViewTest {
}
@Test
- fun `WebView client requests history from configured history delegate`() {
+ fun `WebView client requests history from configured history delegate`() = runTest {
val engineSession = SystemEngineSession(testContext)
val engineView = SystemEngineView(testContext)
@@ -371,14 +373,12 @@ class SystemEngineViewTest {
engineSession.settings.historyTrackingDelegate = historyDelegate
val historyValueCallback: ValueCallback> = mock()
- runBlocking {
- engineSession.webView.webChromeClient!!.getVisitedHistory(historyValueCallback)
- }
+ engineSession.webView.webChromeClient!!.getVisitedHistory(historyValueCallback)
verify(historyValueCallback).onReceiveValue(arrayOf("https://www.mozilla.com"))
}
@Test
- fun `WebView client notifies configured history delegate of title changes`() = runBlocking {
+ fun `WebView client notifies configured history delegate of title changes`() = runTest {
val engineSession = SystemEngineSession(testContext)
val engineView = SystemEngineView(testContext)
diff --git a/components/browser/errorpages/src/main/res/values-ann/strings.xml b/components/browser/errorpages/src/main/res/values-ann/strings.xml
index 786d233546d..e44a54ff0fa 100644
--- a/components/browser/errorpages/src/main/res/values-ann/strings.xml
+++ b/components/browser/errorpages/src/main/res/values-ann/strings.xml
@@ -31,4 +31,65 @@
Òdọdọk…
+
+ Gwu kom (Mîrọ inye)
+
+
+ Chieek Unan ya mè Fo isi
+
+
+ Mîkput ntibi-ntet ya
+
+ Òwọlọ-etip îtibi itet inwọn, ire, mîgbugbana ntibi-ntet ya mgbọ îkiria etip.Soso kpọk sa.
+
+
Môkọt ire akpatan̄ yaìkakup mgbọ keyi mè ìre ìkirọ owuwa ikwaan̄. Kpọk sa me mgbidim mgbọ.
+
Ire òkakọt ìchili akpọk geege, kpọ data okup me okwukwut kwun̄ mè ìyaka ire ntibi-ntet Wi-Fi kwun̄.
+
+ ]]>
+
+
+ Okike mgbọ esun̄be inyi ntibi-ntet yi îraka
+
+
+ Ere akpatan̄ ya edobe ìkanyi ifọọk ntibi-ntet eweekbe, òwọlọ-etip si îta ikukup iban ifọọk.
+
Môkọt ire ere akpatan̄ ìkakup me mgbidim mgbọ mè ìyaka ire ìkirọ owuwa ikwaan̄. Kpọk sa lek me mgbidim mgbọ.
+
Ire òkakọt ìchili akpọk geege, kpọ data òkup me okwukwut kwun̄ mè ìyaka ire Wi-Fi kwun̄.
+
+ ]]>
+
+
+ Ifọọk eyi kpekpọ chieen̄ ònan̄a me òbeme-etip
+
+ Ere akpatan̄ ya ìfọọk me oniin̄ kpekpọ chieen̄, eya orọ òwọlọ-etip ìkayaka ìkọt ìje ìfo isi.
+ ]]>
+
+
+ Akpọk ya ìkagwu ìsi ìtat
+
+
+ Òwọlọ-etip îtele isasa lek me ibọbọkọ inu ya edobe. Ere akpatan̄ ya ìkigwu mbeek ya me otu oniin̄ kporọbe isan̄a.
+
+
Ìre oniin̄ sà ìre ogban cookies eyi akpatan̄ yi okidobe?
+
Ire ichechieek cookies ìkarọ ufialek yi ita, môkọt ire onineen̄ òbeme-etip, ìkare okwukwut kwun̄.
+
+ ]]>
+
diff --git a/components/browser/errorpages/src/main/res/values-ast/strings.xml b/components/browser/errorpages/src/main/res/values-ast/strings.xml
index 84e68d78f51..12a2594a57c 100644
--- a/components/browser/errorpages/src/main/res/values-ast/strings.xml
+++ b/components/browser/errorpages/src/main/res/values-ast/strings.xml
@@ -5,7 +5,7 @@
Retentar
- Nun pue completase la solicitú
+ Nun se pue completar la solicitúLa información adicional tocante a esti problema o fallu nun ta disponible anguaño.]]>
@@ -238,4 +238,7 @@
Problema de sitiu engañosuInformóse de que la páxina web de %1$s ye engañosa y bloquióse según les tos preferencies de seguranza.]]>
-
+
+
+ El sitiu seguru nun ta disponible
+
diff --git a/components/browser/errorpages/src/main/res/values-bg/strings.xml b/components/browser/errorpages/src/main/res/values-bg/strings.xml
index 15e8ae35fcb..dbfddea85f6 100644
--- a/components/browser/errorpages/src/main/res/values-bg/strings.xml
+++ b/components/browser/errorpages/src/main/res/values-bg/strings.xml
@@ -177,4 +177,6 @@
Страницата %1$s е докладвана като измамническа и е блокирана спрямо вашите настройки за безопасност.]]>
-
+
+ Продължаване с HTTP
+
diff --git a/components/browser/errorpages/src/main/res/values-eo/strings.xml b/components/browser/errorpages/src/main/res/values-eo/strings.xml
index 8a55f6aa314..a34416bc5f1 100644
--- a/components/browser/errorpages/src/main/res/values-eo/strings.xml
+++ b/components/browser/errorpages/src/main/res/values-eo/strings.xml
@@ -1,14 +1,9 @@
-
- Problemo dum ŝargado de paĝoKlopodi denove
-
- Reen
-
Ne eblas kompletigi la peton
@@ -234,4 +229,11 @@ la eraro povas esti tempa kaj vi povos klopodi denove poste.
Problemo kun trompa retejoTiu ĉi paĝo ĉe %1$s estis denuncita kiel trompa retejo, kaj ĝi estis blokita surbaze de viaj sekurecaj preferoj.]]>
+
+
+ Sekura retejo ne disponebla
+
+ %1$s ne estas disponebla.]]>
+
+ Daŭrigi al retejo HTTP
diff --git a/components/browser/errorpages/src/main/res/values-gd/strings.xml b/components/browser/errorpages/src/main/res/values-gd/strings.xml
index 22b2dd2f563..95b22874699 100644
--- a/components/browser/errorpages/src/main/res/values-gd/strings.xml
+++ b/components/browser/errorpages/src/main/res/values-gd/strings.xml
@@ -232,4 +232,11 @@
Duilgheadas le làrach foilleChaidh aithris gur e làrach foille a tha san duilleag-lìn seo air %1$s is chaidh bacadh a chur air a-rèir do roghainnean tèarainteachd.]]>
+
+
+ Chan eil làrach thèarainte ri fhaighinn
+
+ %1$s ri fhaighinn.]]>
+
+ Lean air adhart gun làrach-lìn HTTP
diff --git a/components/browser/errorpages/src/main/res/values-ka/strings.xml b/components/browser/errorpages/src/main/res/values-ka/strings.xml
index 44ed3f80d19..9513da6cd65 100644
--- a/components/browser/errorpages/src/main/res/values-ka/strings.xml
+++ b/components/browser/errorpages/src/main/res/values-ka/strings.xml
@@ -116,24 +116,24 @@
-
გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ, ამ ხარვეზის შესახებ.
+
გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ ამ ხარვეზის შესახებ.
]]>დაზიანებული შიგთავსის შეცდომა
- გვერდის ჩვენება, რომლის ნახვასაც ცდილობთ, ვერ ხერხდება, მონაცემთა გადაცემისას აღმოჩენილი შეცდომის გამო
+ გვერდის ჩვენება, რომლის ნახვასაც ცდილობთ, ვერ ხერხდება მონაცემთა გადაცემისას აღმოჩენილი შეცდომის გამო
-
გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ, ამ ხარვეზის შესახებ.
+
გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ ამ ხარვეზის შესახებ.
]]>შიგთავსი დაზიანდა
- გვერდის ჩვენება, რომლის ნახვასაც ცდილობთ, ვერ ხერხდება, მონაცემთა გადაცემისას აღმოჩენილი შეცდომის გამო
+ გვერდის ჩვენება, რომლის ნახვასაც ცდილობთ, ვერ ხერხდება მონაცემთა გადაცემისას აღმოჩენილი შეცდომის გამო
-
გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ, ამ ხარვეზის შესახებ.
+
გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ ამ ხარვეზის შესახებ.
შესაძლოა წაშლილია, გადატანილია ან ფაილზე წვდომის უფლებები შეზღუდულია.
+
შესაძლოა წაშლილია, გადატანილია ან ფაილთან წვდომის უფლებები შეზღუდულია.
]]>
diff --git a/components/browser/errorpages/src/main/res/values-kmr/strings.xml b/components/browser/errorpages/src/main/res/values-kmr/strings.xml
index 880f3127762..477e4b63ec4 100644
--- a/components/browser/errorpages/src/main/res/values-kmr/strings.xml
+++ b/components/browser/errorpages/src/main/res/values-kmr/strings.xml
@@ -216,6 +216,14 @@
Protokola Nenas
+ Navnîşan protokolekê destnîşan dike (mînak, wxyz://) ku gerok wê nas nake, loma gerok nikare bi asayî bi malperê ve bê girîdan.
+
+
Tu dixwazî xwe bigihînî multîmedyayekê yan jî xizmeteke din ya ne nivîskî? Malperê ji bo pêdiviyên zêdek kontrol bike.
+
Dibe ku ji bo hin protokolan beriya gerok wan nas bike, nermalav yan jî pêvekên partiya sêyemîn bivên.
+
+ ]]>
+
Rûpel nehate dîtin
diff --git a/components/browser/errorpages/src/main/res/values-mix/strings.xml b/components/browser/errorpages/src/main/res/values-mix/strings.xml
index 56dbd4dcad8..c5f959f3808 100644
--- a/components/browser/errorpages/src/main/res/values-mix/strings.xml
+++ b/components/browser/errorpages/src/main/res/values-mix/strings.xml
@@ -28,6 +28,12 @@
Ntyityí
+
+ Alguien podría estar intentando imitar el sitio y no deberías continuar.
+
+
+ ]]>Ntyiko
(recomendado)
@@ -37,12 +43,43 @@
Ma ku kitsau
+
+ El navegador se conectó exitosamente, pero se interrumpió la conexión mientras se transfería la información. Vuelva a intentarlo.
+
+
El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelva a intentarlo en unos minutos.
+
Si no puede cargar ninguna página, revise la conexión wifi o de datos de su dispositivo móvil.
+
]]>
+
Koo ña kunu kuntyeu
+
+ El sitio solicitado no respondió a una petición de conexión y el navegador ha dejado de esperar una respuesta.
+
+
¿Podría estar experimentando el servidor una alta demanda o un corte temporal? Vuelva a intentarlo más tarde.
+
¿No puede navegar por otros sitios? Compruebe la conexión de red del equipo.
+
¿Su red o equipo está protegido por un firewall o un proxy? Una configuración incorrecta podría interferir con la navegación web.
+
¿Aun con problemas? Consulte con su administrador de red o proveedor de Internet para obtener asistencia técnica.
+
]]>
+
Kue ku tyitaiña
+
+
+
El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelva a intentarlo en unos minutos.
+
Si no puede cargar ninguna página, revise la conexión wifi o de datos de su dispositivo móvil.
+
+ ]]>
+
+
+ Respuesta inesperada del servidor, y el navegador no puede continuar
+
+ El sitio respondió a la solicitud de red de una forma inesperada y el navegador no puede continuar.
+ ]]>
+
Modo koo conexión
@@ -55,6 +92,14 @@
Tutu vaá
+
+ La página que está intentando ver no puede mostrarse porque se detectó un error en la transmisión de los datos.
+
+
Póngase en contacto con los propietarios del sitio web para informarles de este problema.
+
+ ]]>
+
Maku kuntyeu
diff --git a/components/browser/errorpages/src/main/res/values-skr/strings.xml b/components/browser/errorpages/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..d36069aafe1
--- /dev/null
+++ b/components/browser/errorpages/src/main/res/values-skr/strings.xml
@@ -0,0 +1,290 @@
+
+
+
+
+ ولدا کوشش کرو
+
+
+ ارداس پوری کائنی کر سڳدا
+
+
+ ایں مسئلے بارے وادھوں معلومات فی الحال دستیاب کائنی۔]]>
+
+
+ قابل بھروسہ کنکشن ناکام تھی ڳیا
+
+
+
+
جہڑا ورقہ ݙیکھݨ چاہندے ہو، کائنی ݙکھایا ون٘ڄ سڳدا کیونجو موصول ڈیٹا دی اصلیت دی تصدیق کائنی تھی سڳی۔
+
ویب سائٹ دے مالک کوں ایں مسئلے دا ݙسݨ کیتے انہاں نال رابطہ کرو۔
+
+ ]]>
+
+
+ قابل بھروسہ کنکشن ناکام تھی ڳیا
+
+
+
+
ایہ سرور کانفیگریشن دا کوئی مسئلہ تھی سڳدا ہے یا تھی سڳدے جو کوئی سرور کوں نقلی بݨاوݨ دی کوشش کریندا پیا ہے۔
+
جے تساں ماضی وچ ایں سرور نال جڑے ہو تاں تھی سڳدا ہے جو خرابی کجھ وقت کیتے ہووے تے تساں کجھ دیر بعد وت کوشش کر سڳدے ہو۔
+ ]]>
+
+
+ ودھایا۔۔۔
+
+ تھی سڳدا ہے جو کوئی سائٹ دی نقل بݨیندا پیا ہے تے تساں جاری نہ رکھو۔
+
+ ]]>
+
+ واپس ون٘ڄو(سفارش کیتی ویندی ہے)
+
+ خطرے کوں قبول کرو تے جاری رکھو
+
+
+ کنکشن خراب تھی ڳیا ہائی۔
+
+ براؤزر کامیابی نال کنکٹ تھی ڳیا ہے، پر معلومات دی منتقلی دے دوران کنکشن وچ رکاوٹ پیدا تھئی۔ براہ کرم، ولدا کوشش کرو۔
+
+
سائٹ عارضی طور پر غیر دستیاب یا کافی مصروف تھی سڳدی ہے۔ کجھ دیر بعد وت کوشش کرو۔
+
جے تساں کوئی وی ورقہ لوڈ نہوے کر سڳدے پئے ، تاں آپݨاں ڈیوائس ڈیٹا یا وائی-فائی کنیکشن دی پڑتال کرو۔
+
]]>
+
+
+ کنکشن ٹائم آوٹ تھی ڳیا ہے
+
+
+ ارداس کیتی ڳئی سائٹ نے کنکشن دی ارداس دا جواب کائنی ݙتا تے براؤزر نے جواب دی تانگھ کرݨ بند کر ݙتی ہے۔
+
+
تھی سڳدے جو سرور زیادہ مانگ یا عارضی وقفے دا سامھݨا کریندا پیا ہووے؟ ولدا بعد وچ کوشش کرو۔
+
پھلا تساں ݙوجھی سائٹ تے وی براؤز نہوے کر سڳدے پئے؟ ڈیوائس دے نیٹ ورک کنکشن دی جانچ کرو۔
+
بھلا تہاݙی ڈیوائس یا نیٹ ورک کہیں فائروال یا پراکسی دے ذریعے محفوظ ہے؟غلط ترتیباں ویب براؤز کرݨ وچ مداخلت کر سڳدی ہے۔
+
ہݨ وی مسئلہ درپیش ہے؟ مدد کیتے آپݨے نیٹ ورک کے نگران یا انٹرنیٹ مہیا کار نال رابطہ کرو۔
+
]]>
+
+
+ جڑݨ وچ ناکام ریہا
+
+
+
+
سائٹ عارضی طور تے غیر دستیاب یا کافی مصروف تھی سڳدی ہے۔ کجھ دیر بعد ولدا کوشش کرو۔
+
جے تساں کوئی وی ورقہ لوڈ نہوے کر سڳدے پئے، تاں آپݨی ڈیوائس دا ڈیٹا یا وائی-فائی کنکشن دی جانچ کرو۔
+ ]]>
+
+
+ سرور ولوں غیر متوقع جواب
+
+
+ سائٹ نے نیٹ ورک ارداس دا غیر متوقع طریقے نال جواب ݙتا تے براؤزر جاری کائنی رہ سڳدا۔
+ ]]>
+
+
+ ورقہ ٹھیک طرح ری ڈائریکٹ کائنی تھیندا پیا
+
+
+ براؤزر نے ارداس کیتے ڳئے آئٹم کوں واگزار کرواوݨ دی کوشش روک ݙتی ہے۔ سائٹ ایں ارداس کوں ایں طرحاں ٻئے پاسے بھیڄیندا پیا ہے جہڑی کݙاہیں وی مکمل کائناں تھیسی۔
+
+
بھلا تساں سائٹ کیتے ضروری کوکیاں کوں معذور یا بلاک کر ݙتے؟
+
اگر سائٹ دیاں کوکیاں کوں قبول کرݨ نال وی مسئلہ ٹھیک نئیں تھیندا، تاں ممکن ہے جو ایہ سرور کنفگریشن دا کوئی مسئلہ ہووے نا کہ تہاݙی ڈیوائس دا۔
+
]]>
+
+
+ آف لائن موڈ
+
+
+ براؤزر آپݨے آف لائن موڈ وچ کم کریندا پئے تے ارداس تھئے آئٹم چیز نال کائنی جڑ سڳدا۔
+
+
+
بھلا ڈیوائس فعال نیٹ ورک نال کنکٹ تھیا ہویا ہے؟
+
آن لائن موڈ وچ گھن آوݨ کیتے “ولدا کوشش کرو” دباؤ تے ورقے کوں ولدا لوڈ کرو۔
+
]]>
+
+
+ حفاظتی وجوہات کیتے پورٹ محدود کر ݙتا ڳیا ہے
+
+
+ ارداس کیتا پتہ ہک خاص پورٹ (مثلاً، mozilla.org تے پورٹ 80 کیتے mozilla.org:80)
+ کوں مخصوص کریندا ہے جہڑا عام طور تے ویب براؤز کرݨ دے علاوہ کجھ ٻئے مقاصد کیتے ورتیندے۔ تہاݙی حفاظت تے سلامتی کیتے براؤزر نے ارداس کوں منسوخ کر ݙتا ہے۔]]>
+
+
+ کنکشن ریسٹ تھی ڳیا
+
+
+ کنکشن قائم کرݨ دے دوران نیٹ ورک لنک وچ رکاوٹ پیدا تھئی۔ براہ کرم، ولدا کوشش کرو۔
+
+
سائٹ عارضی طور پر غیر دستیاب یا کافی مصروف تھی سڳدی ہے۔ کجھ دیر بعد وت کوشش کرو۔
+
جے تساں کوئی وی ورقہ لوڈ نہوے کر سڳدے پئے ، تاں آپݨاں ڈیوائس ڈیٹا یا وائی-فائی کنیکشن دی پڑتال کرو۔
ایہ ہٹا یا ٹور ݙتا ڳیا ہوسی یا فائل اجازتاں رسائی کائناں تھیوݨ ݙیندیاں پیاں ہوسن
+
+ ]]>
+
+
+ پراکسی سرور کنکشن دا انکار کر ݙتے
+
+ براؤزر پراکسی سرر ورتݨ کیتے کنفیگر تھیا ہویا ہے، پر پراکسی نے کنکشن دا انکار کر ݙتے۔
+
+
بھلا براؤزر دی پراکسی کنفیگریشن ٹھیک ہے؟ ترتیباں دی پڑتال کرو تے ولدا کوشش کرو۔
+
بھلا پراکسی سروس ایں نیٹ ورک کنوں اجازت ݙیندی ہے؟
+
اڄݨ وی مشکل وچ ہو؟ مدد کیتے نیٹ ورک ایڈمن یا انٹرنیٹ فراہم کرݨ آلے نال مشورہ کرو۔
+
+ ]]>
+
+
+ پراکسی سرور کائنی لبھا
+
+ براؤزر پراکسی سرر ورتݨ کیتے کنفیگر تھیا ہویا ہے، پر پراکسی کائنی لبھ سڳی۔
+
+
بھلا براؤزر دی پراکسی کنفیگریشن ٹھیک ہے؟ ترتیباں دی پڑتال کرو تے ولدا کوشش کرو۔
+
بھلا ڈیوائس فعال نیٹ ورک نال کنکٹ تھئی ہوئی ہے؟
+
اڄݨ وی مشکل وچ ہو؟ مدد کیتے نیٹ ورک ایڈمن یا انٹرنیٹ فراہم کرݨ آلے نال مشورہ کرو۔
+
+ ]]>
+
+
+ مالویئر سائٹ مسئلہ
+
+
+ %1$s تے موجود سائٹ کوں حملہ سائٹ دے طور تے رپورٹ کیتا ڳئے تے تہاݙی سیکیورٹی ترجیحات دی بݨیاد تے بلاک کر ݙتا ڳئے۔
+ ]]>
+
+
+ ناپسندیدہ سائٹ مسئلہ
+
+
+ %1$s تے موجود سائٹ کوں ناپسندیدہ سافٹ ویئر خدمت دے طور تے رپورٹ کیتا ڳئے تے تہاݙی سیکیورٹی ترجیحات دی بݨیاد تے بلاک کر ݙتا ڳئے۔
+ ]]>
+
+
+ نقصان دہ سائٹ مسئلہ
+
+
+ %1$s تے موجود سائٹ کوں ممکنہ نقصان دہ سائٹ دے طور تے رپورٹ کیتا ڳئے تے تہاݙی سیکیورٹی ترجیحات دی بݨیاد تے بلاک کر ݙتا ڳئے۔
+ ]]>
+
+
+ فریبی سائٹ مسئلہ
+
+
+ %1$s تے موجود ویب ورقے کوں فریبی سائٹ دے طور تے رپورٹ کیتا ڳئے تے تہاݙی سیکیورٹی ترجیحات دی بݨیاد تے بلاک کر ݙتا ڳئے۔
+ ]]>
+
+
+ محفوظ سائٹ دستیاب کائنی
+
+ %1$s دا ایچ ٹی ٹی پی ایس ورشن دستیاب کائنی۔]]>
+
+ ایچ ٹی ٹی پی سائٹ تے جاری رکھو
+
diff --git a/components/browser/errorpages/src/main/res/values-tg/strings.xml b/components/browser/errorpages/src/main/res/values-tg/strings.xml
index 39e90f03d73..a0844b1755c 100644
--- a/components/browser/errorpages/src/main/res/values-tg/strings.xml
+++ b/components/browser/errorpages/src/main/res/values-tg/strings.xml
@@ -68,7 +68,7 @@
Эҳтимол сервер бо дархостҳои зиёд машғул аст ё ин ки муваққатан дастнорас аст? Баъдтар аз нав кӯшиш кунед.
]]>
diff --git a/components/browser/errorpages/src/main/res/values-tok/strings.xml b/components/browser/errorpages/src/main/res/values-tok/strings.xml
new file mode 100644
index 00000000000..f274ab55977
--- /dev/null
+++ b/components/browser/errorpages/src/main/res/values-tok/strings.xml
@@ -0,0 +1,83 @@
+
+
+
+ o sin
+
+
+ mi ken ala pini e kama jo
+
+ sona namako pi pakala ni li lon ala.
+]]>
+
+
+ linja len li pakala
+
+
+ linja len li pakala
+
+
+ namako…
+
+
+ ken la jan pi lipu ni li lon ala, li wile ike e sina kepeken nasin len. ni la o tawa ala.
+
+
+]]>
+
+ o tawa pona (sina pona tan ni)
+
+ o awen tawa lipu
+
+
+ linja li pakala
+
+
+ tenpo mute la linja li pakala
+
+
+ mi ken ala tawa lipu
+
+
+ lipu li pana e pakala. nasin la mi sona ala
+
+
+ lipu li toki e ni: o tawa ni. taso tawa ni li pakala
+
+
+ nasin pi linja ala
+
+
+ lipu pi nasin ike
+
+
+
o toki e pakala tawa jan lawa pi lipu ni.
+
+]]>
+
+
+ linja li lon ala
+
+ o lukin e linja sina. o sin e lipu lon tenpo kama.
+
+ o sin
+
+
+ nimi nasin pakala
+
+ nimi nasin li pakala.
+
+
+ lipu li lon ala
+
+
+ sina ken ala lukin e lipu
+
+
+ lipu li wile ike e sina
+
+
+ o tawa lipu pi len ala
+
diff --git a/components/browser/errorpages/src/main/res/values-ug/strings.xml b/components/browser/errorpages/src/main/res/values-ug/strings.xml
index 9816ea7a6b7..7d83a94b129 100644
--- a/components/browser/errorpages/src/main/res/values-ug/strings.xml
+++ b/components/browser/errorpages/src/main/res/values-ug/strings.xml
@@ -19,6 +19,9 @@
قايتىش (تەۋسىيە)
+
+ خەتەرنى قوبۇل قىلىپ داۋاملاشتۇرۇش
+
ئۇلىنىش ئۈزۈلۈپ قالدى
@@ -31,10 +34,27 @@
تورغا ئۇلانمىغان
+
+ قايتا يۈكلە
+
+
+ ئىناۋەتسىز ئادرېسئادرېس ئىناۋەتسىز
+
+ نامەلۇم كېلىشىم
+
+
+ ھۆججەت تېپىلمىدى
+
ھۆججەتنى زىيارەت قىلىش رەت قىلىندى
+
+ ۋاكالەتچى مۇلازىمېتىر ئۇلىنىشنى رەت قىلدى
+
+
+ ۋاكالەتچى مۇلازىمېتىر تېپىلمىدى
+
diff --git a/components/browser/errorpages/src/main/res/values-yo/strings.xml b/components/browser/errorpages/src/main/res/values-yo/strings.xml
new file mode 100644
index 00000000000..35fa07f5593
--- /dev/null
+++ b/components/browser/errorpages/src/main/res/values-yo/strings.xml
@@ -0,0 +1,301 @@
+
+
+
+ Gbìyànjú lẹ́ẹ̀kan sí i
+
+
+ A kò lè parí ìbéèrè rẹ
+
+
+ Àfikún ìfitónilétí nípa ìsòro tàbí àsìṣe yìí kò sí lọ́wọ́ lọ́wọ́ báyìí
+ ]]>
+
+
+ Ìsomọ́ra Onífọ̀kànbalẹ̀ Kùnà
+
+
+
+
ojú-ìwé tí ò ń gbìyànjú àti wò, kò ṣe é fi hàn báyìí nítorí pé ìjẹ́-òtítọ́ data tí a gbà ni a kò lè fi ìdí rẹ̀ múlẹ̀.
+
Jọ̀wọ́ kàn sí ẹni tí ó ni ìkànnì náà láti fi ìsòro yìí tó wọn létí.
+
+ ]]>
+
+
+ Ìsomọ́ra onífọkànbalẹ̀ kùnà
+
+
+
+
Ìsòro yìí lè jẹ́ ìfisáfà ṣiṣẹ́ tàbí kí ẹlòmíràn fẹ́ gbáwọ̀ sáfà wọ̀.
+
Bí o bá ti ṣe àṣeyege àti wọlé sórí sáfà yìí tẹ́lẹ̀ rí, àsìṣe náà lè jẹ́ èyí tí kò ní pẹ́, ó sì le gbìyànjú sí i bó bá yá .
+
+ ]]>
+
+
+ Ìjìnlẹ̀…
+
+ Ẹnìkan le máa gbìyànjú àti gbáwọ̀ ìkànnì náà wọ̀, má tẹ̀síwájú.
+
+
+ ]]>
+
+
+ Padà Sẹ́yìn (Ìmọ̀ràn tó dára)
+
+ Gba Ewu náà kí o sì tẹ̀síwájú
+
+
+ Akùdé bá ìsomọ́ra náà
+
+
+ ìtàkùn ìgbáyé so mọ́ra dáadáa, ṣùgbọ́n àkùdé bá ìsomọ́ra náà nígbà tí à ń fi ìfitónilétí ránṣẹ́. Jọ̀wọ́ gbìyànjú sí i.
+
+
Ìkànnì náà lè má siṣẹ́ fún ìgbà díẹ̀ tàbí kí ọwọ́ kún un. Gbìyànjú rẹ̀ sí i ní àìpẹ́.
+
Bí o kò bá lè jẹ́ kí ojú-ìwé rẹ siṣẹ́, yẹ ohun tí data rẹ fi ń siṣẹ́ wò tàbí ìsomọ́ Wi-Fi rẹ.
+
+ ]]>
+
+
+ Àkókò ìsomọ́ ti kọjá
+
+
+ Ìkànnì tí ó bèèrè kò dáhùn sí ìsomọ́ ti o béèrè àti pé ìtàkùn àgbáyé ti dúró iṣẹ́, ó ń dúró fún èsì.
+
+
Ǹjẹ́ ó ṣe é ṣe kí sáfà máa dojúkọ ìpè púpọ̀ tàbí kí ó má ṣiṣẹ́ fún ìgbà díẹ̀? Gbìyànjú bí ó bá yá.
+
Ǹjẹ́ o ní ìṣòro àti sàwárí àwọn ìkànnì mìíràn? Yẹ ohun tí o fi ṣe àsomọ́ wò.
+
Ǹjẹ́ ìdáabòbò ohun èlò rẹ tàbí nẹ́tíwòkì jẹ́ láti ọwọ́ ojúlówó tàbí asojú? àsìsẹ ààtò lè nípa lórí wíwá nǹkna lórí ìkànnì.
+
O sì ní ìsòro síbẹ̀? Kàn sí alákòóso nẹ́tiwọkì rẹ tàbí àpèse íntánẹ́ẹ̀tì rẹ fún ìrànlọ́wọ́.
+
+ ]]>
+
+
+ Kùnà láti somọ́ra
+
+
+
Ìkànnì náà lè má sí ní àrọ́wọ́tó fún ìgbà díẹ̀ tàbí kí ọwọ́ kún un. Gbìyànjú sí i láìpẹ́.
+
Bí o kò bá lè jẹ́ kí ojú-ìwé siṣẹ́, yẹ ohun èlò data rẹ wò tàbí àsomọ́ Wi-Fi.
+
+ ]]>
+
+
+ Èsì tí a kò retí láti ọ̀dọ̀ sáfà
+
+
+ Ìkànnì ń fèsì sí nẹ́tíwọ̀kì lọ́nà ti a kò retí àti pé ìtàkùn àgbáyé kò le tẹ̀síwájú.
+ ]]>
+
+
+ Ojú-ìwé náà kò ṣe atọ̀nà dáadáa
+
+ ìtàkùn àgbáyé dáwọ́ àtimú àwọn ohun tí a bèèrè dúró. Ìkànnì náà ń ṣe àtúntọ́sọ́nà lọ́nà tí kò lè parí láéláé.
+
+
Ǹjẹ́ o ti ṣo ọ́ di aláìlágbára tàbí dínà àwọn adásiṣẹ́ tí ìkànnì yìí nílò?
+
Bí gbígba àwọn adásiṣẹ́ ìkànnì yìí kò bá yanjú ìsòro yìí, a jẹ́ pé ó ṣe é ṣe kí ó jẹ́ ìsòro ìfisáfàṣiṣẹ́ ni, kìí sì ṣe ohun èlò rẹ.
+
+ ]]>
+
+
+ Ipò àìsí lórí íntánẹ́ẹ̀tì
+
+
+ Ìtàkùn-àgbáyé ń ṣiṣẹ́ lóríipò àìsí lórí íntánẹ́ẹ̀tì, nítorí náà, kò lè so mọ́ ohun tí a bèèrè fún.
+
+
Ǹjẹ́ ohun èlò náà wà ní sísomọ́ nẹ́tíwọ̀kì tó ń ṣiṣẹ́?
+
Tẹ “Gbìyànjú sí i” láti bọ́ sí ipo íntánẹ́ẹ̀tì, kí o si ojú-ìwé náà siṣẹ́ lẹ́ẹ̀kan sí.
+
+ ]]>
+
+
+ Ojú yìí kò ṣiṣẹ́ nítorí ààbò
+
+ Àdírẹ́sì tí o bèèrè fún nílò ojú kan pàtó (e.g., mozilla.org:80 fún ojú 80 lórí mozilla.org) ni a máa ń sábà lò fún àwọn ìdí yàtọ̀ sí ìwá nǹkan kíri orí ìkànnì. Ìtàkùn-àgbáyé ti gbégi lé ìbéérè náà fún ààbò rẹ.
+ ]]>
+
+
+ Ìsomọ́ra di àtúntò
+
+
+ Akùdé bá òpónà nẹ́tíwọ̀kì nígbà tí ìdúnàándúrà ìsomọ́ra ń lọ lọ́wọ́. Jọ̀wọ́ gbìyànjú sí i.
+
+
Ìkànnì náà lè má siṣẹ́ fún ìgbà díẹ̀ tàbí kí ọwọ́ kún un. Gbìyànjú lẹ́yìn ìgbà díẹ̀.
+
Bí o kò bá lè jẹ́ kí ojú ìwé kankan siṣẹ́, yẹ ohun èlò data rẹ wò tàbí ìsomọ́ Wi-Fi.
+
+ ]]>
+
+
+ Ẹ̀yà fáìlì tó léwu
+
+
+
Jọ̀wọ́ kàn sí àwọn tí ó ni ìkànnì láti fi ìsòro yìí tó wọn létí.
+
+ ]]>
+
+
+ Àṣìṣe ìbàjẹ́ àkóónú
+
+
+ Ojú-ìwé tí ò ń gbìyànjú àti wò kò ṣe é fihàn nítorí pé a rí àṣìṣe kan nínú data fífiránṣẹ́.
+
+
Jọ̀wọ́ kan sí àwọn tí ó ni ìkànnì láti fi ìsòro yìí tó wọn létí.
+
+ ]]>
+
+
+ Àkóónú ti bàjẹ́
+ Ojú-ìwé tí ò ń gbíyànjú àti wò kò ṣe é fi hàn nítorí pé, a rí àṣìṣe kan nínú ìfidátà ránsẹ́.
+
+
Jọ̀wọ́ kàn sí àwọn tó ni ìkànnì láti fi ìsòro yìí tó wọn létí.
+
+ ]]>
+
+
+ Àṣìṣe ìṣàrokò àkóónú
+
+ Ojú-iwé tí ò ń gbìyànjú àti wò kò ṣe é fi hàn nítorí tí ó le ìsọdikékeré alápiṣeégbà tàbí aláìfọwọ́sí.
+
+
Jọ̀wọ́ kàn sí àwọn tí wọ́n ni ìkànnì láti fi ìsòro yìí tó wọn létí.
+
+ ]]>
+
+
+ A kò rí àdírẹ́sì
+
+ Ìtàkùn-àgbáyé kò rí agbàlejò sáfà fún àdírẹ́sì.
+
+
yẹ àdírẹ́sì náà wò fún àṣìtẹ̀ gẹ́gẹ́ bí i
+ ww.example.com dípò
+ www.example.com.
+
Bí o kò bá lè mú kí ojú ìwé kankan ṣiṣẹ́, yẹ ohun èlò data rẹ wò tàbí ìsomọ́ Wi-Fi.
+
+ ]]>
+
+
+ Kò sí ìsomọ́ íntánẹ́ẹ̀tì
+
+ Yẹ àsomọ́ nẹ́tiwọ̀kì rẹ wò tàbí gbìyànjú àti mú ojú-ìwé ṣiṣẹ́ lẹ́yìn ìgbà díẹ̀
+
+
+ Tún mú un ṣiṣẹ́
+
+
+ Àdírẹ́sì aláìṣeégbà
+
+ Àdírẹ́sì tí o pèsè kò sí ní ìlànà tí a dámọ̀. Jọ̀wọ́ yẹ àmì ìfi-ọ̀gangan-hàn wò fún àṣìṣe, kì ò sì gbìyànjú lẹ́ẹ̀kan sí i.
+ ]]>
+
+ Àdírẹ́sì náà kò ṣe é gbà
+
+
+
Àdírẹ́sì ìkànnì ni a sábà máa ń kọ bí i http://www.example.com/
+
Rí i dájú pé àkámọ́ asùnsọ́wọ́-wájú ni ò ń lò (b.a. /).
+
+ ]]>
+
+
+ Ìlànà tí a kò mọ̀
+
+ Àdírẹ́sì náà sọ ìlànà kan (e.g., wxyz://) ìtàkùn-àgbáyé kò dá a mọ̀, nítorí náà àtàkùn-àgbáyé kò lè so ó mọ́ ìkànnì.
+
+
Ǹjẹ́ ò gbìyànjú àti ní àǹfààní sí ìbánisọ̀rọ̀-alárànbarà tàbí àwọn iṣẹ́ mìíràn tí kìí ṣe alátẹ̀jíṣẹ́? Wo ìkànnì náà fún àwọn ìbéérè tí ó tún kù.
+
Àwọn ìlànà mìíràn a máa alàgàta amẹ́rọṣiṣẹ́ kí ìtàkùn-àgbáyé tó lè dá wọn mọ̀.
+
+ ]]>
+
+
+ A kò rí fáìlì
+
+
+
Ǹjẹ́ a lè ti pa orúkọ nǹkan náà dà, yọ ọ́ kúrò tàbí mú un lọ síbòmíràn?
+
Ǹjẹ́ àṣìṣe wa lè wà níbi sípẹ́lì, ìlo-lẹ́tà-ńlá, tàbí àṣìtẹ̀ níbi àdírẹ́sì bí?
+
Ǹjẹ́ o ní ìyọ́nda tótó fún ohun tí ò ń bèèrè?
+
+ ]]>
+
+
+ Ìdènà wà sí àṣe sí fáìlì náà
+
+
Ó ṣe é ṣe kí wọ́n ti yọ ọ́, gbé e tàbí àṣẹ sí fáìlì lè máa dènà àti wọlé.
+
+ ]]>
+
+
+ Asojú sáfà kọ asomọ́ra
+
+ Ìtàkùn-àgbáyé ni a ṣe lanà tí ó gbọ́dọ̀ lo asojú fáfà, ṣùgbọ́n asojú náà kọ ìsomọ́ra.
+
+
Ǹjẹ́ ìfiṣiṣẹ́ asojú ìtàkùn-àgbáyé náà tọ̀nà? Yẹ ààtò wò kí o sì gbìyànjú sí i.
+
Ǹjẹ́ iṣẹ́ asojú fàyè gba ìsomọ́ra láti ọ̀dọ̀ nẹ́tíwọ̀kì yìí?
+
O sì ní ìsòro síbẹ̀? Kàn sí alákòóso nẹ́tíwọ̀kì rẹ tàbí olùpèsè íntánẹ́ẹ̀tì rẹ fún ìrànlọ́wọ́.
+
+ ]]>
+
+
+ A kò sí asokú sáfà
+ A ṣe ìtànkùn-àgbáyé láti ṣiṣẹ́ pẹ̀lú asojú sáfà ṣùgbọ́n a kò rí asojú.
+
+
Ǹjẹ́ ìfiṣiṣẹ́ ìtàkùn-àgbáyé tọ̀nà? Yẹ ààtò wò kí o sì gbìyànjú sí i.
+
Ǹjẹ́ ohun èlò rẹ wà ní ìsomọ́ nẹ́tíwọ̀kì tó ń ṣiṣẹ́?
+
Ǹjẹ́ o sì ní ìsòro síbẹ̀? Kàn sí asàkòóso nẹ́tíwọ̀kì rẹ tàbí apèsè íntánẹ́ẹ̀tì rẹ fún ìrànlọ́wọ́.
+
+ ]]>
+
+
+ Ìṣòro ìkànnì mẹ́rọṣisẹ́-onísùtá
+
+
+ Ìkànnì ní %1$s ni wan ti jábọ̀ pé wọ́n ti kọ lù ú, wọ́n sì ti dènà rẹ̀ nítorí ìdáàbòbò tí o yàn.
+ ]]>
+
+
+ Ìṣòro ìkànnì tí a kò fẹ́
+
+ Ìkànnì ní %1$s ni wọ́n jábọ̀ pé ó ní amsọṣiṣẹ́ tí a kò fẹ́, wọ́n sì ti dènà rẹ̀ ítorí ìdáàbòbò tí o yàn.
+ ]]>
+
+
+ Ìṣòro ìkànnì tó léwu
+
+ Ìkànnì ní %1$s ni wọ́n fi sùn pé ó jẹ́ ìkànnì tí ó ṣe é ṣe kí ó léwu, wọ́n sì di dènà rẹ̀ nítorí ìdàábòbò tí o yàn.
+ ]]>
+
+
+ Ìṣòro ìkànnì atànnijẹ
+
+
+ Ojú-ìwé ìkànnì yìí %1$s ni wọ́n ti fi sùn gẹ́gẹ́ ìkànnì àtànnijẹ, wọ́n sì ti dènà rẹ̀ nítorí ìdàábòbò tí o yàn.
+ ]]>
+
+
+ Ìkànnì adánilójú kò sí
+
+ %1$s kò sí.]]>
+
+ Tẹ̀síwájú sí ìkànnì HTTP
+
diff --git a/components/browser/icons/build.gradle b/components/browser/icons/build.gradle
index 3777dfe554e..c7670778039 100644
--- a/components/browser/icons/build.gradle
+++ b/components/browser/icons/build.gradle
@@ -77,6 +77,7 @@ dependencies {
androidTestImplementation Dependencies.androidx_test_core
androidTestImplementation Dependencies.androidx_test_runner
androidTestImplementation Dependencies.androidx_test_rules
+ androidTestImplementation Dependencies.testing_coroutines
}
apply from: '../../../publish.gradle'
diff --git a/components/browser/icons/src/androidTest/java/mozilla/components/browser/icons/OnDeviceBrowserIconsTest.kt b/components/browser/icons/src/androidTest/java/mozilla/components/browser/icons/OnDeviceBrowserIconsTest.kt
index 9dfd0fefd4e..826a3a186a3 100644
--- a/components/browser/icons/src/androidTest/java/mozilla/components/browser/icons/OnDeviceBrowserIconsTest.kt
+++ b/components/browser/icons/src/androidTest/java/mozilla/components/browser/icons/OnDeviceBrowserIconsTest.kt
@@ -6,7 +6,8 @@ package mozilla.components.browser.icons
import android.content.Context
import androidx.test.core.app.ApplicationProvider
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.icons.generator.IconGenerator
import mozilla.components.concept.engine.manifest.Size
import mozilla.components.concept.fetch.Client
@@ -20,8 +21,9 @@ class OnDeviceBrowserIconsTest {
private val context: Context
get() = ApplicationProvider.getApplicationContext()
+ @OptIn(ExperimentalCoroutinesApi::class)
@Test
- fun dataUriLoad() = runBlocking {
+ fun dataUriLoad() = runTest {
val request = IconRequest(
url = "https://www.mozilla.org",
size = IconRequest.Size.DEFAULT,
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/BrowserIconsTest.kt b/components/browser/icons/src/test/java/mozilla/components/browser/icons/BrowserIconsTest.kt
index f946a6b375c..bce6aefdb18 100644
--- a/components/browser/icons/src/test/java/mozilla/components/browser/icons/BrowserIconsTest.kt
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/BrowserIconsTest.kt
@@ -10,8 +10,8 @@ import android.os.Looper.getMainLooper
import android.widget.ImageView
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
-import kotlinx.coroutines.runBlocking
import mozilla.components.browser.icons.generator.IconGenerator
import mozilla.components.concept.engine.manifest.Size
import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
@@ -20,6 +20,8 @@ import mozilla.components.support.test.eq
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okio.Okio
@@ -28,6 +30,7 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertSame
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
@@ -39,9 +42,13 @@ import org.mockito.Mockito.verify
import org.robolectric.Shadows.shadowOf
import java.io.OutputStream
+@ExperimentalCoroutinesApi // for runTestOnMain
@RunWith(AndroidJUnit4::class)
class BrowserIconsTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
@Before
@After
fun cleanUp() {
@@ -50,7 +57,7 @@ class BrowserIconsTest {
}
@Test
- fun `Uses generator`() {
+ fun `Uses generator`() = runTestOnMain {
val mockedIcon: Icon = mock()
val generator: IconGenerator = mock()
@@ -60,13 +67,13 @@ class BrowserIconsTest {
val icon = BrowserIcons(testContext, httpClient = mock(), generator = generator)
.loadIcon(request)
- assertEquals(mockedIcon, runBlocking { icon.await() })
+ assertEquals(mockedIcon, icon.await())
verify(generator).generate(testContext, request)
}
@Test
- fun `WHEN resources are provided THEN an icon will be downloaded from one of them`() = runBlocking {
+ fun `WHEN resources are provided THEN an icon will be downloaded from one of them`() = runTestOnMain {
val server = MockWebServer()
server.enqueue(
@@ -119,7 +126,7 @@ class BrowserIconsTest {
}
@Test
- fun `WHEN icon is loaded twice THEN second load is delivered from memory cache`() = runBlocking {
+ fun `WHEN icon is loaded twice THEN second load is delivered from memory cache`() = runTestOnMain {
val server = MockWebServer()
server.enqueue(
@@ -162,7 +169,7 @@ class BrowserIconsTest {
}
@Test
- fun `WHEN icon is loaded again and not in memory cache THEN second load is delivered from disk cache`() = runBlocking {
+ fun `WHEN icon is loaded again and not in memory cache THEN second load is delivered from disk cache`() = runTestOnMain {
val server = MockWebServer()
server.enqueue(
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageHandlerTest.kt b/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageHandlerTest.kt
index 24c3f20a252..a2a105ecf73 100644
--- a/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageHandlerTest.kt
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageHandlerTest.kt
@@ -7,9 +7,10 @@ package mozilla.components.browser.icons.extension
import android.graphics.Bitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.icons.Icon
import mozilla.components.browser.icons.IconRequest
@@ -34,10 +35,11 @@ import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class IconMessageHandlerTest {
+ @ExperimentalCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
@Test
fun `Complex message (TheVerge) is transformed into IconRequest and loaded`() {
- runBlocking {
+ runTest {
val bitmap: Bitmap = mock()
val icon = Icon(bitmap, source = Icon.Source.DOWNLOAD)
val deferredIcon = GlobalScope.async { icon }
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/generator/DefaultIconGeneratorTest.kt b/components/browser/icons/src/test/java/mozilla/components/browser/icons/generator/DefaultIconGeneratorTest.kt
index b07dc4c0e42..15a9f7824b2 100644
--- a/components/browser/icons/src/test/java/mozilla/components/browser/icons/generator/DefaultIconGeneratorTest.kt
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/generator/DefaultIconGeneratorTest.kt
@@ -6,7 +6,6 @@ package mozilla.components.browser.icons.generator
import android.graphics.Bitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
import mozilla.components.browser.icons.Icon
import mozilla.components.browser.icons.IconRequest
import mozilla.components.support.ktx.android.util.dpToPx
@@ -43,7 +42,7 @@ class DefaultIconGeneratorTest {
}
@Test
- fun generate() = runBlocking {
+ fun generate() {
val generator = DefaultIconGenerator()
val icon = generator.generate(
diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/NonBlockingHttpIconLoaderTest.kt b/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/NonBlockingHttpIconLoaderTest.kt
index 61e3de9872a..f89779d293f 100644
--- a/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/NonBlockingHttpIconLoaderTest.kt
+++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/NonBlockingHttpIconLoaderTest.kt
@@ -6,8 +6,6 @@ package mozilla.components.browser.icons.loader
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.icons.Icon
import mozilla.components.browser.icons.IconRequest
import mozilla.components.concept.fetch.Client
@@ -20,6 +18,8 @@ import mozilla.components.support.test.any
import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.Assert.assertEquals
@@ -27,6 +27,7 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.doReturn
@@ -41,10 +42,12 @@ import java.io.InputStream
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class NonBlockingHttpIconLoaderTest {
- val scope = TestCoroutineScope()
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
@Test
- fun `Loader will return IconLoader#Result#NoResult for a load request and respond with the result through a callback`() = runBlockingTest {
+ fun `Loader will return IconLoader#Result#NoResult for a load request and respond with the result through a callback`() = runTestOnMain {
val clients = listOf(
HttpURLConnectionClient(),
OkHttpClient()
@@ -97,7 +100,7 @@ class NonBlockingHttpIconLoaderTest {
}
@Test
- fun `Loader will not perform any requests for data uris`() = runBlockingTest {
+ fun `Loader will not perform any requests for data uris`() = runTestOnMain {
val client: Client = mock()
var callbackIconRequest: IconRequest? = null
var callbackResource: IconRequest.Resource? = null
@@ -125,7 +128,7 @@ class NonBlockingHttpIconLoaderTest {
}
@Test
- fun `Request has timeouts applied`() = runBlockingTest {
+ fun `Request has timeouts applied`() = runTestOnMain {
val client: Client = mock()
val loader = NonBlockingHttpIconLoader(client, scope) { _, _, _ -> }
doReturn(
@@ -154,7 +157,7 @@ class NonBlockingHttpIconLoaderTest {
}
@Test
- fun `NoResult is returned for non-successful requests`() = runBlockingTest {
+ fun `NoResult is returned for non-successful requests`() = runTestOnMain {
val client: Client = mock()
var callbackIconRequest: IconRequest? = null
var callbackResource: IconRequest.Resource? = null
@@ -189,7 +192,7 @@ class NonBlockingHttpIconLoaderTest {
}
@Test
- fun `Loader will not try to load URL again that just recently failed`() = runBlockingTest {
+ fun `Loader will not try to load URL again that just recently failed`() = runTestOnMain {
val client: Client = mock()
val loader = NonBlockingHttpIconLoader(client, scope) { _, _, _ -> }
doReturn(
@@ -218,7 +221,7 @@ class NonBlockingHttpIconLoaderTest {
}
@Test
- fun `Loader will return NoResult for IOExceptions happening during fetch`() = runBlockingTest {
+ fun `Loader will return NoResult for IOExceptions happening during fetch`() = runTestOnMain {
val client: Client = mock()
doThrow(IOException("Mock")).`when`(client).fetch(any())
var callbackIconRequest: IconRequest? = null
@@ -244,7 +247,7 @@ class NonBlockingHttpIconLoaderTest {
}
@Test
- fun `Loader will return NoResult for IOExceptions happening during toIconLoaderResult`() = runBlockingTest {
+ fun `Loader will return NoResult for IOExceptions happening during toIconLoaderResult`() = runTestOnMain {
val client: Client = mock()
var callbackIconRequest: IconRequest? = null
var callbackResource: IconRequest.Resource? = null
@@ -282,7 +285,7 @@ class NonBlockingHttpIconLoaderTest {
}
@Test
- fun `Loader will sanitize URL`() = runBlockingTest {
+ fun `Loader will sanitize URL`() = runTestOnMain {
val client: Client = mock()
val captor = argumentCaptor()
val loader = NonBlockingHttpIconLoader(client, scope) { _, _, _ -> }
diff --git a/components/browser/menu/src/main/res/values-skr/strings.xml b/components/browser/menu/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..979a43dd3fe
--- /dev/null
+++ b/components/browser/menu/src/main/res/values-skr/strings.xml
@@ -0,0 +1,11 @@
+
+
+
+ مینیو
+
+ نمایاں کیتا ڳیا
+
+ ایڈ ــ آن
+
+ ایڈ ــ آن منیجر
+
diff --git a/components/browser/menu/src/main/res/values-yo/strings.xml b/components/browser/menu/src/main/res/values-yo/strings.xml
new file mode 100644
index 00000000000..84804823652
--- /dev/null
+++ b/components/browser/menu/src/main/res/values-yo/strings.xml
@@ -0,0 +1,11 @@
+
+
+
+ Mẹ́nù
+
+ Afàmìsí
+
+ Àfikún
+
+ Àsàmójútó Àfikún
+
diff --git a/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuTest.kt b/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuTest.kt
index 81d420309a8..b493018fe42 100644
--- a/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuTest.kt
+++ b/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuTest.kt
@@ -46,7 +46,6 @@ class WebExtensionBrowserMenuTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val testDispatcher = coroutinesTestRule.testDispatcher
@Before
fun setup() {
@@ -84,8 +83,6 @@ class WebExtensionBrowserMenuTest {
val adapter = BrowserMenuAdapter(testContext, items)
val menu = WebExtensionBrowserMenu(adapter, store)
- testDispatcher.advanceUntilIdle()
-
val anchor = Button(testContext)
val popup = menu.show(anchor)
@@ -434,7 +431,6 @@ class WebExtensionBrowserMenuTest {
val menu: WebExtensionBrowserMenu = mock()
menuItem.bind(menu, view)
- testDispatcher.advanceUntilIdle()
CollectionProcessor.withFactCollection { facts ->
container.performClick()
diff --git a/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItemTest.kt b/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItemTest.kt
index 89e3388a13e..90c9c0868fd 100644
--- a/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItemTest.kt
+++ b/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItemTest.kt
@@ -14,7 +14,7 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.menu.R
import mozilla.components.browser.menu.WebExtensionBrowserMenu
import mozilla.components.concept.engine.webextension.Action
@@ -40,7 +40,7 @@ class WebExtensionBrowserMenuItemTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val testDispatcher = coroutinesTestRule.testDispatcher
+ private val dispatcher = coroutinesTestRule.testDispatcher
@Test
fun `web extension menu item is visible by default`() {
@@ -85,7 +85,7 @@ class WebExtensionBrowserMenuItemTest {
val action = WebExtensionBrowserMenuItem(browserAction, {})
action.bind(mock(), view)
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertFalse(view.isEnabled)
}
@@ -118,7 +118,7 @@ class WebExtensionBrowserMenuItemTest {
val action = WebExtensionBrowserMenuItem(browserAction, {})
action.bind(mock(), view)
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
val iconCaptor = argumentCaptor()
verify(imageView).setImageDrawable(iconCaptor.capture())
@@ -158,7 +158,7 @@ class WebExtensionBrowserMenuItemTest {
val action = WebExtensionBrowserMenuItem(browserAction, {})
action.bind(mock(), view)
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(badgeView).setBadgeText(badgeText)
assertEquals(View.INVISIBLE, badgeView.visibility)
@@ -189,7 +189,7 @@ class WebExtensionBrowserMenuItemTest {
val action = WebExtensionBrowserMenuItem(browserAction, {})
action.bind(mock(), view)
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(imageView).setImageDrawable(notNull())
}
@@ -225,7 +225,7 @@ class WebExtensionBrowserMenuItemTest {
val menu: WebExtensionBrowserMenu = mock()
item.bind(menu, view)
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
container.performClick()
@@ -262,7 +262,7 @@ class WebExtensionBrowserMenuItemTest {
val menu: WebExtensionBrowserMenu = mock()
item.bind(menu, view)
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(labelView).text = "title"
verify(badgeView).text = "badgeText"
@@ -286,7 +286,7 @@ class WebExtensionBrowserMenuItemTest {
}
@Test
- fun `GIVEN setIcon was called, WHEN bind is called, icon setup uses the tint set`() = runBlocking {
+ fun `GIVEN setIcon was called, WHEN bind is called, icon setup uses the tint set`() = runTest {
val webExtMenuItem = spy(WebExtensionBrowserMenuItem(mock(), mock()))
val testIconTintColorResource = R.color.accent_material_dark
val menu: WebExtensionBrowserMenu = mock()
diff --git a/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt b/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt
index be249708ef4..2d7ab77e4c4 100644
--- a/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt
+++ b/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt
@@ -7,7 +7,6 @@ package mozilla.components.browser.menu2
import android.view.View
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.PopupWindow
-import androidx.coordinatorlayout.widget.CoordinatorLayout
import mozilla.components.browser.menu2.ext.displayPopup
import mozilla.components.browser.menu2.view.MenuView
import mozilla.components.concept.menu.MenuController
@@ -46,7 +45,6 @@ class BrowserMenuController(
anchor: View,
orientation: Orientation?
): PopupWindow {
- val desiredOrientation = orientation ?: determineMenuOrientation(anchor.parent as? View?)
val view = MenuView(anchor.context).apply {
// Show nested list if present, or the standard menu candidates list.
submitList(menuCandidates)
@@ -58,12 +56,12 @@ class BrowserMenuController(
view.onDismiss = ::dismiss
view.onReopenMenu = ::reopenMenu
setOnDismissListener(menuDismissListener)
- displayPopup(view, anchor, desiredOrientation)
+ displayPopup(view, anchor, orientation)
}.also {
currentPopupInfo = PopupMenuInfo(
window = it,
anchor = anchor,
- orientation = desiredOrientation,
+ orientation = orientation,
nested = null
)
}
@@ -134,18 +132,7 @@ class BrowserMenuController(
private data class PopupMenuInfo(
val window: MenuPopupWindow,
val anchor: View,
- val orientation: Orientation,
+ val orientation: Orientation?,
val nested: NestedMenuCandidate? = null
)
}
-
-/**
- * Determines the orientation to be used for a menu
- * based on the positioning of the [parent] in the layout.
- */
-fun determineMenuOrientation(parent: View?): Orientation {
- val params = parent?.layoutParams as? CoordinatorLayout.LayoutParams
- ?: return Orientation.DOWN
-
- return Orientation.fromGravity(params.gravity)
-}
diff --git a/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/PopupWindow.kt b/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/PopupWindow.kt
index 4b9f8e10f2b..24aba9ee3bc 100644
--- a/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/PopupWindow.kt
+++ b/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/PopupWindow.kt
@@ -11,12 +11,11 @@ import android.widget.PopupWindow
import androidx.core.widget.PopupWindowCompat
import mozilla.components.browser.menu2.R
import mozilla.components.concept.menu.Orientation
-import mozilla.components.support.ktx.android.view.isRTL
internal fun PopupWindow.displayPopup(
containerView: View,
anchor: View,
- preferredOrientation: Orientation
+ preferredOrientation: Orientation? = null,
) {
// Measure menu
val spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
@@ -38,14 +37,12 @@ internal fun PopupWindow.displayPopup(
preferredOrientation == Orientation.DOWN && fitsDown ->
showPopupWithDownOrientation(anchor, reversed)
preferredOrientation == Orientation.UP && fitsUp ->
- showPopupWithUpOrientation(anchor, availableHeightToBottom, containerHeight, reversed)
+ showPopupWithUpOrientation(anchor, containerHeight, reversed)
else -> {
showPopupWhereBestFits(
anchor,
fitsUp,
fitsDown,
- availableHeightToTop,
- availableHeightToBottom,
reversed,
containerHeight
)
@@ -58,74 +55,58 @@ private fun PopupWindow.showPopupWhereBestFits(
anchor: View,
fitsUp: Boolean,
fitsDown: Boolean,
- availableHeightToTop: Int,
- availableHeightToBottom: Int,
reversed: Boolean,
containerHeight: Int
) {
- // We don't have enough space to show the menu UP neither DOWN.
- // Let's just show the popup at the location of the anchor.
- if (!fitsUp && !fitsDown) {
- showAtAnchorLocation(anchor, availableHeightToTop < availableHeightToBottom, reversed)
- } else {
- if (fitsDown) {
- showPopupWithDownOrientation(anchor, reversed)
- } else {
- showPopupWithUpOrientation(anchor, availableHeightToBottom, containerHeight, reversed)
- }
+ when {
+ // Not enough space to show the menu UP neither DOWN.
+ // Let's just show the popup at the location of the anchor.
+ !fitsUp && !fitsDown -> showAtAnchorLocation(anchor, reversed)
+ // Enough space to show menu down
+ fitsDown -> showPopupWithDownOrientation(anchor, reversed)
+ // Otherwise, show menu up
+ else -> showPopupWithUpOrientation(anchor, containerHeight, reversed)
}
}
private fun PopupWindow.showPopupWithUpOrientation(
anchor: View,
- availableHeightToBottom: Int,
containerHeight: Int,
reversed: Boolean,
) {
- val xOffset = if (anchor.isRTL) -anchor.width else 0
animationStyle = if (reversed) {
R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom
} else {
R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom
}
- // Positioning the menu above and overlapping the anchor.
- val yOffset = if (availableHeightToBottom < 0) {
- // The anchor is partially below of the bottom of the screen, let's make the menu completely visible.
- availableHeightToBottom - containerHeight
- } else {
- -containerHeight
- }
- showAsDropDown(anchor, xOffset, yOffset)
+ val yOffset = -containerHeight
+ showAsDropDown(anchor, 0, yOffset)
}
private fun PopupWindow.showPopupWithDownOrientation(anchor: View, reversed: Boolean) {
- val xOffset = if (anchor.isRTL) -anchor.width else 0
+ // Apply the best fit animation style based on positioning
animationStyle = if (reversed) {
R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop
} else {
R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop
}
- // Menu should overlay the anchor.
- showAsDropDown(anchor, xOffset, -anchor.height)
+
+ PopupWindowCompat.setOverlapAnchor(this, true)
+ showAsDropDown(anchor)
}
-private fun PopupWindow.showAtAnchorLocation(anchor: View, isCloserToTop: Boolean, reversed: Boolean) {
+private fun PopupWindow.showAtAnchorLocation(
+ anchor: View,
+ reversed: Boolean,
+) {
val anchorPosition = IntArray(2)
// Apply the best fit animation style based on positioning
- animationStyle = if (isCloserToTop) {
- if (reversed) {
- R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop
- } else {
- R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop
- }
+ animationStyle = if (reversed) {
+ R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft
} else {
- if (reversed) {
- R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom
- } else {
- R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom
- }
+ R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRight
}
anchor.getLocationOnScreen(anchorPosition)
diff --git a/components/browser/menu2/src/main/res/anim/menu_enter_left.xml b/components/browser/menu2/src/main/res/anim/menu_enter_left.xml
new file mode 100644
index 00000000000..de510ecb12f
--- /dev/null
+++ b/components/browser/menu2/src/main/res/anim/menu_enter_left.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/components/browser/menu2/src/main/res/anim/menu_enter_right.xml b/components/browser/menu2/src/main/res/anim/menu_enter_right.xml
new file mode 100644
index 00000000000..0ca98399a7c
--- /dev/null
+++ b/components/browser/menu2/src/main/res/anim/menu_enter_right.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/components/browser/menu2/src/main/res/values-skr/strings.xml b/components/browser/menu2/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..92d8c77be32
--- /dev/null
+++ b/components/browser/menu2/src/main/res/values-skr/strings.xml
@@ -0,0 +1,7 @@
+
+
+
+ مینیو
+
+ نمایاں کیتا ڳیا
+
diff --git a/components/browser/menu2/src/main/res/values/style.xml b/components/browser/menu2/src/main/res/values/style.xml
index 6c894d04d2a..eb2702e6f1b 100644
--- a/components/browser/menu2/src/main/res/values/style.xml
+++ b/components/browser/menu2/src/main/res/values/style.xml
@@ -74,5 +74,15 @@
@anim/menu_enter_right_bottom@anim/menu_exit
+
+
+
+
diff --git a/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt b/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt
index fa3b4ea277e..4668aa558be 100644
--- a/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt
+++ b/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt
@@ -4,16 +4,11 @@
package mozilla.components.browser.menu2
-import android.view.Gravity
-import android.view.View
import android.widget.Button
-import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.concept.menu.MenuController
-import mozilla.components.concept.menu.Orientation
import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
import mozilla.components.concept.menu.candidate.MenuCandidate
-import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -79,40 +74,4 @@ class BrowserMenuControllerTest {
assertFalse(popup.isShowing)
assertTrue(dismissed)
}
-
- @Test
- fun `determineMenuOrientation returns Orientation-DOWN by default`() {
- assertEquals(
- Orientation.DOWN,
- determineMenuOrientation(mock())
- )
- }
-
- @Test
- fun `determineMenuOrientation returns Orientation-UP for views with bottom gravity in CoordinatorLayout`() {
- val params = CoordinatorLayout.LayoutParams(100, 100)
- params.gravity = Gravity.BOTTOM
-
- val view = View(testContext)
- view.layoutParams = params
-
- assertEquals(
- Orientation.UP,
- determineMenuOrientation(view)
- )
- }
-
- @Test
- fun `determineMenuOrientation returns Orientation-DOWN for views with top gravity in CoordinatorLayout`() {
- val params = CoordinatorLayout.LayoutParams(100, 100)
- params.gravity = Gravity.TOP
-
- val view = View(testContext)
- view.layoutParams = params
-
- assertEquals(
- Orientation.DOWN,
- determineMenuOrientation(view)
- )
- }
}
diff --git a/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHoldersTest.kt b/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHoldersTest.kt
index 7c7ebb2dcfd..929ac8df0e4 100644
--- a/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHoldersTest.kt
+++ b/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHoldersTest.kt
@@ -12,7 +12,6 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.menu2.R
import mozilla.components.concept.menu.Side
import mozilla.components.concept.menu.candidate.AsyncDrawableMenuIcon
@@ -23,6 +22,7 @@ import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -40,7 +40,6 @@ class DrawableMenuIconViewHoldersTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val testDispatcher = coroutinesTestRule.testDispatcher
private lateinit var parent: ConstraintLayout
private lateinit var layoutInflater: LayoutInflater
@@ -85,7 +84,7 @@ class DrawableMenuIconViewHoldersTest {
}
@Test
- fun `async view holder sets icon on view`() = testDispatcher.runBlockingTest {
+ fun `async view holder sets icon on view`() = runTestOnMain {
val holder = AsyncDrawableMenuIconViewHolder(parent, layoutInflater, Side.END)
val drawable = mock()
@@ -95,7 +94,7 @@ class DrawableMenuIconViewHoldersTest {
}
@Test
- fun `async view holder uses loading icon and fallback icon`() = testDispatcher.runBlockingTest {
+ fun `async view holder uses loading icon and fallback icon`() = runTestOnMain {
val logger = mock()
val holder = AsyncDrawableMenuIconViewHolder(parent, layoutInflater, Side.END, logger)
diff --git a/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/PopupWindowTest.kt b/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/PopupWindowTest.kt
index 16eca214910..31162029b26 100644
--- a/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/PopupWindowTest.kt
+++ b/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/PopupWindowTest.kt
@@ -49,7 +49,7 @@ class PopupWindowTest {
popupWindow.displayPopup(menuContentView, anchor, Orientation.DOWN)
assertEquals(R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, popupWindow.animationStyle)
- verify(popupWindow).showAsDropDown(anchor, 0, -10)
+ verify(popupWindow).showAsDropDown(anchor, 0, 0)
}
@Test
@@ -58,7 +58,7 @@ class PopupWindowTest {
popupWindow.displayPopup(menuContentView, anchor, Orientation.DOWN)
assertEquals(R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop, popupWindow.animationStyle)
- verify(popupWindow).showAsDropDown(anchor, 0, -10)
+ verify(popupWindow).showAsDropDown(anchor, 0, 0)
}
@Test
@@ -67,7 +67,7 @@ class PopupWindowTest {
popupWindow.displayPopup(menuContentView, anchor, Orientation.DOWN)
assertEquals(R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop, popupWindow.animationStyle)
- verify(popupWindow).showAsDropDown(anchor, -10, -10)
+ verify(popupWindow).showAsDropDown(anchor, 0, 0)
}
@Test
@@ -76,7 +76,7 @@ class PopupWindowTest {
popupWindow.displayPopup(menuContentView, anchor, Orientation.UP)
assertEquals(R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightTop, popupWindow.animationStyle)
- verify(popupWindow).showAsDropDown(anchor, 0, -10)
+ verify(popupWindow).showAsDropDown(anchor, 0, 0)
}
@Test
@@ -103,7 +103,7 @@ class PopupWindowTest {
popupWindow.displayPopup(menuContentView, anchor, Orientation.UP)
assertEquals(R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftBottom, popupWindow.animationStyle)
- verify(popupWindow).showAsDropDown(anchor, -10, -90)
+ verify(popupWindow).showAsDropDown(anchor, 0, -90)
}
@Test
@@ -121,7 +121,7 @@ class PopupWindowTest {
popupWindow.displayPopup(menuContentView, anchor, Orientation.UP)
assertEquals(R.style.Mozac_Browser_Menu2_Animation_OverflowMenuRightBottom, popupWindow.animationStyle)
- verify(popupWindow).showAsDropDown(anchor, 0, -110)
+ verify(popupWindow).showAsDropDown(anchor, 0, -90)
}
@Test
@@ -129,8 +129,8 @@ class PopupWindowTest {
anchor = createMockViewWith(x = 0, y = 10, false)
doReturn(Int.MAX_VALUE).`when`(menuContentView).measuredHeight
- popupWindow.displayPopup(menuContentView, anchor, Orientation.DOWN)
- assertEquals(R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeftTop, popupWindow.animationStyle)
+ popupWindow.displayPopup(menuContentView, anchor, Orientation.UP)
+ assertEquals(R.style.Mozac_Browser_Menu2_Animation_OverflowMenuLeft, popupWindow.animationStyle)
verify(popupWindow).showAtLocation(anchor, Gravity.START or Gravity.TOP, 0, 10)
}
diff --git a/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/ViewTest.kt b/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/ViewTest.kt
index 237200dc22c..0e2067c9e7b 100644
--- a/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/ViewTest.kt
+++ b/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/ViewTest.kt
@@ -8,7 +8,6 @@ import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.Drawable
-import android.os.Build
import android.view.View
import android.widget.ImageView
import android.widget.TextView
@@ -30,7 +29,6 @@ import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
-import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
class ViewTest {
@@ -115,7 +113,6 @@ class ViewTest {
verify(view).imageTintList = null
}
- @Config(sdk = [Build.VERSION_CODES.M])
@Test
fun `sets highlight effect`() {
val view: View = mock()
diff --git a/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/AutoSaveTest.kt b/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/AutoSaveTest.kt
index 5f17cdabcb0..92a5614939f 100644
--- a/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/AutoSaveTest.kt
+++ b/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/AutoSaveTest.kt
@@ -8,10 +8,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
@@ -23,9 +20,12 @@ import mozilla.components.support.test.eq
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertNotSame
import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.`when`
@@ -40,9 +40,14 @@ import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class AutoSaveTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
@Test
fun `AutoSave - when going to background`() {
- runBlocking {
+ runTestOnMain {
// Keep the "owner" in scope to avoid it getting garbage collected and therefore lifecycle events
// not getting propagated (See #1428).
val owner = mock(LifecycleOwner::class.java)
@@ -77,22 +82,19 @@ class AutoSaveTest {
@Test
fun `AutoSave - when tab gets added`() {
- runBlocking {
+ runTestOnMain {
val state = BrowserState()
val store = BrowserStore(state)
val sessionStorage: SessionStorage = mock()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val autoSave = AutoSave(
store = store,
sessionStorage = sessionStorage,
minimumIntervalMs = 0
).whenSessionsChange(scope)
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(autoSave.saveJob)
verify(sessionStorage, never()).save(any())
@@ -103,7 +105,7 @@ class AutoSaveTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
autoSave.saveJob?.join()
@@ -113,7 +115,7 @@ class AutoSaveTest {
@Test
fun `AutoSave - when tab gets removed`() {
- runBlocking {
+ runTestOnMain {
val sessionStorage: SessionStorage = mock()
val store = BrowserStore(
@@ -126,23 +128,20 @@ class AutoSaveTest {
)
)
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val autoSave = AutoSave(
store = store,
sessionStorage = sessionStorage,
minimumIntervalMs = 0
).whenSessionsChange(scope)
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(autoSave.saveJob)
verify(sessionStorage, never()).save(any())
store.dispatch(TabListAction.RemoveTabAction("mozilla")).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
autoSave.saveJob?.join()
@@ -152,7 +151,7 @@ class AutoSaveTest {
@Test
fun `AutoSave - when all tabs get removed`() {
- runBlocking {
+ runTestOnMain {
val store = BrowserStore(
BrowserState(
tabs = listOf(
@@ -165,23 +164,20 @@ class AutoSaveTest {
val sessionStorage: SessionStorage = mock()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val autoSave = AutoSave(
store = store,
sessionStorage = sessionStorage,
minimumIntervalMs = 0
).whenSessionsChange(scope)
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(autoSave.saveJob)
verify(sessionStorage, never()).save(any())
store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
autoSave.saveJob?.join()
@@ -191,7 +187,7 @@ class AutoSaveTest {
@Test
fun `AutoSave - when no tabs are left`() {
- runBlocking {
+ runTestOnMain {
val store = BrowserStore(
BrowserState(
tabs = listOf(createTab("https://www.firefox.com", id = "firefox")),
@@ -201,22 +197,19 @@ class AutoSaveTest {
val sessionStorage: SessionStorage = mock()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val autoSave = AutoSave(
store = store,
sessionStorage = sessionStorage,
minimumIntervalMs = 0
).whenSessionsChange(scope)
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(autoSave.saveJob)
verify(sessionStorage, never()).save(any())
store.dispatch(TabListAction.RemoveTabAction("firefox")).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
autoSave.saveJob?.join()
@@ -226,7 +219,7 @@ class AutoSaveTest {
@Test
fun `AutoSave - when tab gets selected`() {
- runBlocking {
+ runTestOnMain {
val store = BrowserStore(
BrowserState(
tabs = listOf(
@@ -239,23 +232,20 @@ class AutoSaveTest {
val sessionStorage: SessionStorage = mock()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val autoSave = AutoSave(
store = store,
sessionStorage = sessionStorage,
minimumIntervalMs = 0
).whenSessionsChange(scope)
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(autoSave.saveJob)
verify(sessionStorage, never()).save(any())
store.dispatch(TabListAction.SelectTabAction("mozilla")).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
autoSave.saveJob?.join()
@@ -265,7 +255,7 @@ class AutoSaveTest {
@Test
fun `AutoSave - when tab loading state changes`() {
- runBlocking {
+ runTestOnMain {
val sessionStorage: SessionStorage = mock()
val store = BrowserStore(
@@ -277,9 +267,6 @@ class AutoSaveTest {
)
)
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val autoSave = AutoSave(
store = store,
sessionStorage = sessionStorage,
@@ -293,7 +280,7 @@ class AutoSaveTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(autoSave.saveJob)
verify(sessionStorage, never()).save(any())
@@ -305,7 +292,7 @@ class AutoSaveTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
autoSave.saveJob?.join()
diff --git a/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/FileEngineSessionStateStorageTest.kt b/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/FileEngineSessionStateStorageTest.kt
index 68383a456bf..317124d226b 100644
--- a/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/FileEngineSessionStateStorageTest.kt
+++ b/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/FileEngineSessionStateStorageTest.kt
@@ -5,7 +5,7 @@
package mozilla.components.browser.session.storage
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.support.test.fakes.engine.FakeEngine
import mozilla.components.support.test.fakes.engine.FakeEngineSessionState
import mozilla.components.support.test.robolectric.testContext
@@ -18,11 +18,11 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class FileEngineSessionStateStorageTest {
@Test
- fun `able to read and write engine session states`() = runBlocking {
+ fun `able to read and write engine session states`() = runTest {
val storage = FileEngineSessionStateStorage(testContext, FakeEngine())
// reading non-existing tab
- assertNull(runBlocking { storage.read("test-tab") })
+ assertNull(storage.read("test-tab"))
storage.write("test-tab", FakeEngineSessionState("some-engine-state"))
val state = storage.read("test-tab")
@@ -33,7 +33,7 @@ class FileEngineSessionStateStorageTest {
}
@Test
- fun `able to delete specific engine session states`() = runBlocking {
+ fun `able to delete specific engine session states`() = runTest {
val storage = FileEngineSessionStateStorage(testContext, FakeEngine())
storage.write("test-tab-1", FakeEngineSessionState("some-engine-state-1"))
storage.write("test-tab-2", FakeEngineSessionState("some-engine-state-2"))
@@ -63,7 +63,7 @@ class FileEngineSessionStateStorageTest {
}
@Test
- fun `able to delete all engine states`() = runBlocking {
+ fun `able to delete all engine states`() = runTest {
val storage = FileEngineSessionStateStorage(testContext, FakeEngine())
// already empty storage
diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt
index df1138d4733..f699e027433 100644
--- a/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt
+++ b/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt
@@ -596,6 +596,15 @@ sealed class ContentAction : BrowserAction() {
data class ConsumePromptRequestAction(val sessionId: String, val promptRequest: PromptRequest) :
ContentAction()
+ /**
+ * Replaces a prompt request from [ContentState] with [promptRequest] based on the [previousPromptUid].
+ */
+ data class ReplacePromptRequestAction(
+ val sessionId: String,
+ val previousPromptUid: String,
+ val promptRequest: PromptRequest
+ ) : ContentAction()
+
/**
* Adds a [FindResultState] to the [ContentState] with the given [sessionId].
*/
diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineObserver.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineObserver.kt
index 948995c5852..000b66143f8 100644
--- a/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineObserver.kt
+++ b/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineObserver.kt
@@ -266,6 +266,12 @@ internal class EngineObserver(
)
}
+ override fun onPromptUpdate(previousPromptRequestUid: String, promptRequest: PromptRequest) {
+ store.dispatch(
+ ContentAction.ReplacePromptRequestAction(tabId, previousPromptRequestUid, promptRequest)
+ )
+ }
+
override fun onRepostPromptCancelled() {
store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(tabId, true))
}
diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddleware.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddleware.kt
new file mode 100644
index 00000000000..b659956e76e
--- /dev/null
+++ b/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddleware.kt
@@ -0,0 +1,80 @@
+/* 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.state.engine.middleware
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.SessionPriority.DEFAULT
+import mozilla.components.concept.engine.EngineSession.SessionPriority.HIGH
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.MiddlewareContext
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * [Middleware] implementation responsible for updating the priority of the selected [EngineSession]
+ * to [HIGH] and the rest to [DEFAULT].
+ */
+class SessionPrioritizationMiddleware : Middleware {
+ private val logger = Logger("SessionPrioritizationMiddleware")
+
+ @VisibleForTesting
+ internal var previousHighestPriorityTabId = ""
+
+ override fun invoke(
+ context: MiddlewareContext,
+ next: (BrowserAction) -> Unit,
+ action: BrowserAction
+ ) {
+
+ when (action) {
+ is EngineAction.UnlinkEngineSessionAction -> {
+ val activeTab = context.state.findTab(action.tabId)
+ activeTab?.engineState?.engineSession?.updateSessionPriority(DEFAULT)
+ logger.info("Update the tab ${activeTab?.id} priority to ${DEFAULT.name}")
+ }
+ else -> {
+ // no-op
+ }
+ }
+
+ next(action)
+
+ when (action) {
+ is TabListAction,
+ is EngineAction.LinkEngineSessionAction -> {
+ val state = context.state
+ if (previousHighestPriorityTabId != state.selectedTabId) {
+ updatePriorityIfNeeded(state)
+ }
+ }
+ else -> {
+ // no-op
+ }
+ }
+ }
+
+ private fun updatePriorityIfNeeded(state: BrowserState) {
+ val currentSelectedTab = state.selectedTabId?.let { state.findTab(it) }
+ val previousSelectedTab = state.findTab(previousHighestPriorityTabId)
+ val currentEngineSession: EngineSession? = currentSelectedTab?.engineState?.engineSession
+
+ // We need to make sure, we alter the previousHighestPriorityTabId, after the session is linked.
+ // So we update the priority on the engine session, as we could get actions where the tab
+ // is selected but not linked yet, causing out sync issues,
+ // when previousHighestPriorityTabId didn't call updateSessionPriority()
+ if (currentEngineSession != null) {
+ previousSelectedTab?.engineState?.engineSession?.updateSessionPriority(DEFAULT)
+ currentEngineSession.updateSessionPriority(HIGH)
+ logger.info("Update the currentSelectedTab ${currentSelectedTab.id} priority to ${HIGH.name}")
+ logger.info("Update the previousSelectedTab ${previousSelectedTab?.id} priority to ${DEFAULT.name}")
+ previousHighestPriorityTabId = currentSelectedTab.id
+ }
+ }
+}
diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt
index 48f07cc4ed3..b04a9b3f125 100644
--- a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt
+++ b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt
@@ -113,6 +113,15 @@ internal object ContentStateReducer {
is ContentAction.ConsumePromptRequestAction -> updateContentState(state, action.sessionId) {
it.copy(promptRequests = it.promptRequests - action.promptRequest)
}
+ is ContentAction.ReplacePromptRequestAction -> updateContentState(
+ state,
+ action.sessionId
+ ) { contentState ->
+ val updated = contentState.promptRequests
+ .filter { it.uid != action.previousPromptUid }
+ .plus(action.promptRequest)
+ contentState.copy(promptRequests = updated)
+ }
is ContentAction.AddFindResultAction -> updateContentState(state, action.sessionId) {
it.copy(findResults = it.findResults + action.findResult)
}
diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/search/SearchEngine.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/search/SearchEngine.kt
index 2b632493af1..5be12fa4c15 100644
--- a/components/browser/state/src/main/java/mozilla/components/browser/state/search/SearchEngine.kt
+++ b/components/browser/state/src/main/java/mozilla/components/browser/state/search/SearchEngine.kt
@@ -46,6 +46,11 @@ data class SearchEngine(
* A custom search engine added by the user.
*/
CUSTOM,
+
+ /**
+ * A search engine add by the application.
+ */
+ APPLICATION,
}
// Cache these parameters to avoid repeated parsing.
diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/state/SearchState.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/state/SearchState.kt
index 5d37ba35805..23b9c0df022 100644
--- a/components/browser/state/src/main/java/mozilla/components/browser/state/state/SearchState.kt
+++ b/components/browser/state/src/main/java/mozilla/components/browser/state/state/SearchState.kt
@@ -13,6 +13,7 @@ import mozilla.components.browser.state.search.SearchEngine
* @property region The region of the user.
* @property regionSearchEngines The list of bundled [SearchEngine]s for the "home" region of the user.
* @property customSearchEngines The list of custom [SearchEngine]s, added by the user.
+ * @property applicationSearchEngines The list of optional [SearchEngine]s, added by application.
* @property additionalSearchEngines Additional [SearchEngine]s that the application decided to load
* and that the user explicitly added to their list of search engines.
* @property additionalAvailableSearchEngines Additional [SearchEngine]s that the application decided
@@ -33,6 +34,7 @@ data class SearchState(
val region: RegionState? = null,
val regionSearchEngines: List = emptyList(),
val customSearchEngines: List = emptyList(),
+ val applicationSearchEngines: List = emptyList(),
val additionalSearchEngines: List = emptyList(),
val additionalAvailableSearchEngines: List = emptyList(),
val hiddenSearchEngines: List = emptyList(),
@@ -47,7 +49,7 @@ data class SearchState(
* The list of search engines to be used for searches (bundled and custom search engines).
*/
val SearchState.searchEngines: List
- get() = (regionSearchEngines + additionalSearchEngines + customSearchEngines)
+ get() = (regionSearchEngines + additionalSearchEngines + customSearchEngines + applicationSearchEngines)
/**
* The list of search engines that are available for the user to be added to their list of search
diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineMiddlewareTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineMiddlewareTest.kt
index 72a690a93dc..b3ead147e76 100644
--- a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineMiddlewareTest.kt
+++ b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineMiddlewareTest.kt
@@ -4,12 +4,6 @@
package mozilla.components.browser.state.engine
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.engine.middleware.TrimMemoryMiddleware
import mozilla.components.browser.state.state.BrowserState
@@ -19,32 +13,18 @@ import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertTrue
-import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.verify
class EngineMiddlewareTest {
- private lateinit var dispatcher: TestCoroutineDispatcher
- private lateinit var scope: CoroutineScope
-
- @Before
- fun setUp() {
- dispatcher = TestCoroutineDispatcher()
- scope = CoroutineScope(dispatcher)
-
- Dispatchers.setMain(dispatcher)
- }
-
- @After
- fun tearDown() {
- dispatcher.cleanupTestCoroutines()
- scope.cancel()
-
- Dispatchers.resetMain()
- }
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
@Test
fun `Dispatching CreateEngineSessionAction multiple times should only create one engine session`() {
@@ -69,7 +49,7 @@ class EngineMiddlewareTest {
EngineAction.CreateEngineSessionAction("mozilla")
)
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine, Mockito.times(1)).createSession(false, null)
diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineObserverTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineObserverTest.kt
index bfe325ca42c..1eb98d885c9 100644
--- a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineObserverTest.kt
+++ b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineObserverTest.kt
@@ -9,7 +9,7 @@ import android.graphics.Bitmap
import android.view.WindowManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.Job
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.CrashAction
@@ -821,7 +821,7 @@ class EngineObserverTest {
}
@Test
- fun engineSessionObserverWithContentPermissionRequests() {
+ fun engineSessionObserverWithContentPermissionRequests() = runTest {
val permissionRequest: PermissionRequest = mock()
val store: BrowserStore = mock()
val observer = EngineObserver("tab-id", store)
@@ -831,14 +831,12 @@ class EngineObserverTest {
)
doReturn(Job()).`when`(store).dispatch(action)
- runBlockingTest {
- observer.onContentPermissionRequest(permissionRequest)
- verify(store).dispatch(action)
- }
+ observer.onContentPermissionRequest(permissionRequest)
+ verify(store).dispatch(action)
}
@Test
- fun engineSessionObserverWithAppPermissionRequests() {
+ fun engineSessionObserverWithAppPermissionRequests() = runTest {
val permissionRequest: PermissionRequest = mock()
val store: BrowserStore = mock()
val observer = EngineObserver("tab-id", store)
@@ -847,10 +845,8 @@ class EngineObserverTest {
permissionRequest
)
- runBlockingTest {
- observer.onAppPermissionRequest(permissionRequest)
- verify(store).dispatch(action)
- }
+ observer.onAppPermissionRequest(permissionRequest)
+ verify(store).dispatch(action)
}
@Test
@@ -868,6 +864,23 @@ class EngineObserverTest {
)
}
+ @Test
+ fun engineObserverHandlesOnPromptUpdate() {
+ val promptRequest: PromptRequest = mock()
+ val store: BrowserStore = mock()
+ val observer = EngineObserver("tab-id", store)
+ val previousPromptUID = "prompt-uid"
+
+ observer.onPromptUpdate(previousPromptUID, promptRequest)
+ verify(store).dispatch(
+ ContentAction.ReplacePromptRequestAction(
+ "tab-id",
+ previousPromptUID,
+ promptRequest
+ )
+ )
+ }
+
@Test
fun engineObserverHandlesWindowRequest() {
val windowRequest: WindowRequest = mock()
diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CrashMiddlewareTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CrashMiddlewareTest.kt
index 159082ae65b..bf436ad1d2e 100644
--- a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CrashMiddlewareTest.kt
+++ b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CrashMiddlewareTest.kt
@@ -4,8 +4,6 @@
package mozilla.components.browser.state.engine.middleware
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.state.action.CrashAction
import mozilla.components.browser.state.engine.EngineMiddleware
import mozilla.components.browser.state.state.BrowserState
@@ -16,12 +14,19 @@ import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
+import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.doReturn
class CrashMiddlewareTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
@Test
fun `Crash and restore scenario`() {
val engineSession1: EngineSession = mock()
@@ -29,8 +34,6 @@ class CrashMiddlewareTest {
val engineSession3: EngineSession = mock()
val engine: Engine = mock()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
val store = BrowserStore(
middleware = EngineMiddleware.create(
@@ -75,7 +78,7 @@ class CrashMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertFalse(store.state.tabs[0].engineState.crashed)
assertFalse(store.state.tabs[1].engineState.crashed)
@@ -88,7 +91,7 @@ class CrashMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
// Restoring unknown session
store.dispatch(
@@ -97,7 +100,7 @@ class CrashMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertFalse(store.state.tabs[0].engineState.crashed)
assertFalse(store.state.tabs[1].engineState.crashed)
@@ -110,9 +113,6 @@ class CrashMiddlewareTest {
val engine: Engine = mock()
doReturn(engineSession).`when`(engine).createSession()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val store = BrowserStore(
middleware = EngineMiddleware.create(
engine = engine,
@@ -131,7 +131,7 @@ class CrashMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertTrue(store.state.tabs[0].engineState.crashed)
@@ -141,7 +141,7 @@ class CrashMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertFalse(store.state.tabs[0].engineState.crashed)
}
diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CreateEngineSessionMiddlewareTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CreateEngineSessionMiddlewareTest.kt
index fff2b379768..1370d26e22d 100644
--- a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CreateEngineSessionMiddlewareTest.kt
+++ b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CreateEngineSessionMiddlewareTest.kt
@@ -5,9 +5,6 @@
package mozilla.components.browser.state.engine.middleware
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.selector.findCustomTab
@@ -23,10 +20,12 @@ import mozilla.components.support.test.any
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import mozilla.components.support.test.whenever
-import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
@@ -38,17 +37,13 @@ import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class CreateEngineSessionMiddlewareTest {
-
- private val dispatcher = TestCoroutineDispatcher()
- private val scope = CoroutineScope(dispatcher)
-
- @After
- fun tearDown() {
- dispatcher.cleanupTestCoroutines()
- }
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
@Test
- fun `creates engine session if needed`() = runBlocking {
+ fun `creates engine session if needed`() = runTestOnMain {
val engine: Engine = mock()
val engineSession: EngineSession = mock()
whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)
@@ -63,19 +58,19 @@ class CreateEngineSessionMiddlewareTest {
store.dispatch(EngineAction.CreateEngineSessionAction(tab.id)).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(engine, times(1)).createSession(false)
assertEquals(engineSession, store.state.findTab(tab.id)?.engineState?.engineSession)
store.dispatch(EngineAction.CreateEngineSessionAction(tab.id)).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(engine, times(1)).createSession(false)
assertEquals(engineSession, store.state.findTab(tab.id)?.engineState?.engineSession)
}
@Test
- fun `restores engine session state if available`() = runBlocking {
+ fun `restores engine session state if available`() = runTestOnMain {
val engine: Engine = mock()
val engineSession: EngineSession = mock()
whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)
@@ -92,14 +87,14 @@ class CreateEngineSessionMiddlewareTest {
store.dispatch(EngineAction.UpdateEngineSessionStateAction(tab.id, engineSessionState)).joinBlocking()
store.dispatch(EngineAction.CreateEngineSessionAction(tab.id)).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(engineSession).restoreState(engineSessionState)
Unit
}
@Test
- fun `creates no engine session if tab does not exist`() = runBlocking {
+ fun `creates no engine session if tab does not exist`() = runTestOnMain {
val engine: Engine = mock()
`when`(engine.createSession(anyBoolean(), anyString())).thenReturn(mock())
@@ -111,14 +106,14 @@ class CreateEngineSessionMiddlewareTest {
store.dispatch(EngineAction.CreateEngineSessionAction("invalid")).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(engine, never()).createSession(anyBoolean(), any())
Unit
}
@Test
- fun `creates no engine session if session does not exist`() = runBlocking {
+ fun `creates no engine session if session does not exist`() = runTestOnMain {
val engine: Engine = mock()
`when`(engine.createSession(anyBoolean(), anyString())).thenReturn(mock())
@@ -135,14 +130,14 @@ class CreateEngineSessionMiddlewareTest {
).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(engine, never()).createSession(anyBoolean(), any())
Unit
}
@Test
- fun `dispatches follow-up action after engine session is created`() = runBlocking {
+ fun `dispatches follow-up action after engine session is created`() = runTestOnMain {
val engine: Engine = mock()
val engineSession: EngineSession = mock()
whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)
@@ -159,7 +154,7 @@ class CreateEngineSessionMiddlewareTest {
store.dispatch(EngineAction.CreateEngineSessionAction(tab.id, followupAction = followupAction)).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(engine, times(1)).createSession(false)
assertEquals(engineSession, store.state.findTab(tab.id)?.engineState?.engineSession)
@@ -167,7 +162,7 @@ class CreateEngineSessionMiddlewareTest {
}
@Test
- fun `dispatches follow-up action once engine session is created by pending action`() = runBlocking {
+ fun `dispatches follow-up action once engine session is created by pending action`() = runTestOnMain {
val engine: Engine = mock()
val engineSession: EngineSession = mock()
whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)
@@ -187,7 +182,7 @@ class CreateEngineSessionMiddlewareTest {
store.dispatch(EngineAction.CreateEngineSessionAction(tab.id, followupAction = followupAction))
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(engine, times(1)).createSession(false)
assertEquals(engineSession, store.state.findTab(tab.id)?.engineState?.engineSession)
@@ -212,7 +207,7 @@ class CreateEngineSessionMiddlewareTest {
store.dispatch(EngineAction.CreateEngineSessionAction(customTab.id, followupAction = followupAction)).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(engine, times(1)).createSession(false)
assertEquals(engineSession, store.state.findCustomTab(customTab.id)?.engineState?.engineSession)
diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/EngineDelegateMiddlewareTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/EngineDelegateMiddlewareTest.kt
index 5d9177ea14a..5ca91c52811 100644
--- a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/EngineDelegateMiddlewareTest.kt
+++ b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/EngineDelegateMiddlewareTest.kt
@@ -4,8 +4,6 @@
package mozilla.components.browser.state.engine.middleware
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.engine.EngineMiddleware
import mozilla.components.browser.state.state.BrowserState
@@ -18,8 +16,10 @@ import mozilla.components.concept.engine.EngineSession
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
+import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mockito.doReturn
@@ -28,15 +28,17 @@ import org.mockito.Mockito.times
import org.mockito.Mockito.verify
class EngineDelegateMiddlewareTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
@Test
fun `LoadUrlAction for tab without engine session`() {
val engineSession: EngineSession = mock()
val engine: Engine = mock()
doReturn(engineSession).`when`(engine).createSession()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val tab = createTab("https://www.mozilla.org", id = "test-tab")
val store = BrowserStore(
middleware = EngineMiddleware.create(
@@ -55,7 +57,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine).createSession(private = false, contextId = null)
@@ -69,9 +71,6 @@ class EngineDelegateMiddlewareTest {
val engine: Engine = mock()
doReturn(engineSession).`when`(engine).createSession(private = true)
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val tab = createTab("https://www.mozilla.org", id = "test-tab", private = true)
val store = BrowserStore(
middleware = EngineMiddleware.create(
@@ -90,7 +89,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine).createSession(private = true, contextId = null)
@@ -104,9 +103,6 @@ class EngineDelegateMiddlewareTest {
val engine: Engine = mock()
doReturn(engineSession).`when`(engine).createSession(contextId = "test-container")
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val tab = createTab("https://www.mozilla.org", id = "test-tab", contextId = "test-container")
val store = BrowserStore(
middleware = EngineMiddleware.create(
@@ -125,7 +121,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine).createSession(private = false, contextId = "test-container")
@@ -138,9 +134,6 @@ class EngineDelegateMiddlewareTest {
val engineSession: EngineSession = mock()
val engine: Engine = mock()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val store = BrowserStore(
middleware = EngineMiddleware.create(
engine = engine,
@@ -162,7 +155,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine, never()).createSession(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyString())
@@ -175,9 +168,6 @@ class EngineDelegateMiddlewareTest {
val engineSession: EngineSession = mock()
val engine: Engine = mock()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val store = BrowserStore(
middleware = EngineMiddleware.create(
engine = engine,
@@ -199,7 +189,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine, never()).createSession(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyString())
@@ -212,9 +202,6 @@ class EngineDelegateMiddlewareTest {
val engineSession: EngineSession = mock()
val engine: Engine = mock()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val store = BrowserStore(
middleware = EngineMiddleware.create(
engine = engine,
@@ -236,7 +223,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine, never()).createSession(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyString())
@@ -252,9 +239,6 @@ class EngineDelegateMiddlewareTest {
val parentEngineSession: EngineSession = mock()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val parent = createTab("https://getpocket.com", id = "parent-tab").copy(
engineState = EngineState(parentEngineSession)
)
@@ -277,7 +261,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine).createSession(private = false, contextId = null)
@@ -292,9 +276,6 @@ class EngineDelegateMiddlewareTest {
val engine: Engine = mock()
doReturn(engineSession).`when`(engine).createSession()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val parent = createTab("https://getpocket.com", id = "parent-tab")
val tab = createTab("https://www.mozilla.org", id = "test-tab", parent = parent)
@@ -315,7 +296,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine, times(1)).createSession(private = false, contextId = null)
@@ -327,9 +308,6 @@ class EngineDelegateMiddlewareTest {
fun `LoadUrlAction with flags and additional headers`() {
val engineSession: EngineSession = mock()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val store = BrowserStore(
middleware = EngineMiddleware.create(
engine = mock(),
@@ -356,7 +334,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engineSession, times(1)).loadUrl(
@@ -376,9 +354,6 @@ class EngineDelegateMiddlewareTest {
val engine: Engine = mock()
doReturn(engineSession).`when`(engine).createSession()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val tab = createTab("https://www.mozilla.org", id = "test-tab")
val store = BrowserStore(
@@ -398,7 +373,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine).createSession(private = false, contextId = null)
@@ -411,9 +386,6 @@ class EngineDelegateMiddlewareTest {
fun `LoadUrlAction for not existing tab`() {
val engine: Engine = mock()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val store = BrowserStore(
middleware = EngineMiddleware.create(
engine = engine,
@@ -433,7 +405,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine, never()).createSession(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyString())
@@ -446,9 +418,6 @@ class EngineDelegateMiddlewareTest {
val engine: Engine = mock()
doReturn(engineSession).`when`(engine).createSession()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val tab = createTab("https://www.mozilla.org", id = "test-tab")
val store = BrowserStore(
middleware = EngineMiddleware.create(
@@ -469,7 +438,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine).createSession(private = false, contextId = null)
@@ -487,9 +456,6 @@ class EngineDelegateMiddlewareTest {
val engine: Engine = mock()
doReturn(engineSession).`when`(engine).createSession()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val tab = createTab("https://www.mozilla.org", id = "test-tab")
val store = BrowserStore(
middleware = EngineMiddleware.create(
@@ -508,7 +474,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine).createSession(private = false, contextId = null)
@@ -524,9 +490,6 @@ class EngineDelegateMiddlewareTest {
val engine: Engine = mock()
doReturn(engineSession).`when`(engine).createSession()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val tab = createTab("https://www.mozilla.org", id = "test-tab")
val store = BrowserStore(
middleware = EngineMiddleware.create(
@@ -544,7 +507,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine).createSession(private = false, contextId = null)
@@ -558,9 +521,6 @@ class EngineDelegateMiddlewareTest {
val engine: Engine = mock()
doReturn(engineSession).`when`(engine).createSession()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val tab = createTab("https://www.mozilla.org", id = "test-tab")
val store = BrowserStore(
middleware = EngineMiddleware.create(
@@ -578,7 +538,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine).createSession(private = false, contextId = null)
@@ -592,9 +552,6 @@ class EngineDelegateMiddlewareTest {
val engine: Engine = mock()
doReturn(engineSession).`when`(engine).createSession()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val tab = createTab("https://www.mozilla.org", id = "test-tab")
val store = BrowserStore(
middleware = EngineMiddleware.create(
@@ -613,7 +570,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine).createSession(private = false, contextId = null)
@@ -627,9 +584,6 @@ class EngineDelegateMiddlewareTest {
val engine: Engine = mock()
doReturn(engineSession).`when`(engine).createSession()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val tab = createTab("https://www.mozilla.org", id = "test-tab")
val store = BrowserStore(
middleware = EngineMiddleware.create(
@@ -648,7 +602,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine).createSession(private = false, contextId = null)
@@ -662,9 +616,6 @@ class EngineDelegateMiddlewareTest {
val engine: Engine = mock()
doReturn(engineSession).`when`(engine).createSession()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val tab = createTab("https://www.mozilla.org", id = "test-tab")
val store = BrowserStore(
middleware = EngineMiddleware.create(
@@ -683,7 +634,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine).createSession(private = false, contextId = null)
@@ -697,9 +648,6 @@ class EngineDelegateMiddlewareTest {
val engine: Engine = mock()
doReturn(engineSession).`when`(engine).createSession()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val tab = createTab("https://www.mozilla.org", id = "test-tab")
val store = BrowserStore(
middleware = EngineMiddleware.create(
@@ -717,7 +665,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine).createSession(private = false, contextId = null)
@@ -731,9 +679,6 @@ class EngineDelegateMiddlewareTest {
val engine: Engine = mock()
doReturn(engineSession).`when`(engine).createSession()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val tab = createTab("https://www.mozilla.org", id = "test-tab")
val store = BrowserStore(
middleware = EngineMiddleware.create(
@@ -752,7 +697,7 @@ class EngineDelegateMiddlewareTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(engine).createSession(private = false, contextId = null)
@@ -765,9 +710,6 @@ class EngineDelegateMiddlewareTest {
val engineSession1: EngineSession = mock()
val engineSession2: EngineSession = mock()
- val dispatcher = TestCoroutineDispatcher()
- val scope = CoroutineScope(dispatcher)
-
val store = BrowserStore(
middleware = EngineMiddleware.create(
engine = mock(),
@@ -795,7 +737,7 @@ class EngineDelegateMiddlewareTest {
store.dispatch(EngineAction.PurgeHistoryAction).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(engineSession1).purgeHistory()
verify(engineSession2).purgeHistory()
diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/LinkingMiddlewareTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/LinkingMiddlewareTest.kt
index fee75ad499b..ae15ff183ab 100644
--- a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/LinkingMiddlewareTest.kt
+++ b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/LinkingMiddlewareTest.kt
@@ -5,13 +5,6 @@
package mozilla.components.browser.state.engine.middleware
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.selector.findTab
@@ -23,11 +16,12 @@ import mozilla.components.support.test.any
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
-import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.anyString
@@ -36,24 +30,10 @@ import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class LinkingMiddlewareTest {
- private lateinit var dispatcher: TestCoroutineDispatcher
- private lateinit var scope: CoroutineScope
-
- @Before
- fun setUp() {
- dispatcher = TestCoroutineDispatcher()
- scope = CoroutineScope(dispatcher)
-
- Dispatchers.setMain(dispatcher)
- }
-
- @After
- fun tearDown() {
- dispatcher.cleanupTestCoroutines()
- scope.cancel()
-
- Dispatchers.resetMain()
- }
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
@Test
fun `loads URL after linking`() {
@@ -68,7 +48,7 @@ class LinkingMiddlewareTest {
val engineSession: EngineSession = mock()
store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession)).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(engineSession).loadUrl(tab.content.url)
}
@@ -92,7 +72,7 @@ class LinkingMiddlewareTest {
val childEngineSession: EngineSession = mock()
store.dispatch(EngineAction.LinkEngineSessionAction(child.id, childEngineSession)).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(childEngineSession).loadUrl(child.content.url, parentEngineSession)
}
@@ -116,7 +96,7 @@ class LinkingMiddlewareTest {
val childEngineSession: EngineSession = mock()
store.dispatch(EngineAction.LinkEngineSessionAction(child.id, childEngineSession)).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(childEngineSession).loadUrl(child.content.url)
}
@@ -134,7 +114,7 @@ class LinkingMiddlewareTest {
val engineSession: EngineSession = mock()
store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession, skipLoading = true)).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(engineSession, never()).loadUrl(tab.content.url)
}
@@ -151,13 +131,13 @@ class LinkingMiddlewareTest {
val engineSession: EngineSession = mock()
store.dispatch(EngineAction.LinkEngineSessionAction("invalid", engineSession)).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(engineSession, never()).loadUrl(anyString(), any(), any(), any())
}
@Test
- fun `registers engine observer after linking`() = runBlocking {
+ fun `registers engine observer after linking`() = runTestOnMain {
val tab1 = createTab("https://www.mozilla.org", id = "1")
val tab2 = createTab("https://www.mozilla.org", id = "2")
@@ -187,7 +167,7 @@ class LinkingMiddlewareTest {
}
@Test
- fun `unregisters engine observer before unlinking`() = runBlocking {
+ fun `unregisters engine observer before unlinking`() = runTestOnMain {
val tab1 = createTab("https://www.mozilla.org", id = "1")
val tab2 = createTab("https://www.mozilla.org", id = "2")
@@ -212,7 +192,7 @@ class LinkingMiddlewareTest {
}
@Test
- fun `registers engine observer when tab is added with engine session`() = runBlocking {
+ fun `registers engine observer when tab is added with engine session`() = runTestOnMain {
val engineSession: EngineSession = mock()
val tab1 = createTab("https://www.mozilla.org", id = "1")
val tab2 = createTab("https://www.mozilla.org", id = "2", engineSession = engineSession)
diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddlewareTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddlewareTest.kt
new file mode 100644
index 00000000000..eba60ac168f
--- /dev/null
+++ b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddlewareTest.kt
@@ -0,0 +1,118 @@
+/* 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.state.engine.middleware
+
+import mozilla.components.browser.state.action.EngineAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSession.SessionPriority.DEFAULT
+import mozilla.components.concept.engine.EngineSession.SessionPriority.HIGH
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+class SessionPrioritizationMiddlewareTest {
+
+ @Test
+ fun `GIVEN a linked session WHEN UnlinkEngineSessionAction THEN set the DEFAULT priority to the unlinked tab`() {
+ val middleware = SessionPrioritizationMiddleware()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "1"),
+ )
+ ),
+ middleware = listOf(middleware)
+ )
+ val engineSession1: EngineSession = mock()
+
+ store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking()
+ store.dispatch(EngineAction.UnlinkEngineSessionAction("1")).joinBlocking()
+
+ verify(engineSession1).updateSessionPriority(DEFAULT)
+ assertEquals("", middleware.previousHighestPriorityTabId)
+ }
+
+ @Test
+ fun `GIVEN a previous selected tab WHEN LinkEngineSessionAction THEN update the selected linked tab priority to HIGH`() {
+ val middleware = SessionPrioritizationMiddleware()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "1"),
+ )
+ ),
+ middleware = listOf(middleware)
+ )
+ val engineSession1: EngineSession = mock()
+
+ store.dispatch(TabListAction.SelectTabAction("1")).joinBlocking()
+
+ assertEquals("", middleware.previousHighestPriorityTabId)
+
+ store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking()
+
+ assertEquals("1", middleware.previousHighestPriorityTabId)
+ verify(engineSession1).updateSessionPriority(HIGH)
+ }
+
+ @Test
+ fun `GIVEN a previous selected tab with priority DEFAULT WHEN selecting and linking a new tab THEN update the previous tab to DEFAULT and the new one to HIGH`() {
+ val middleware = SessionPrioritizationMiddleware()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "1"),
+ createTab("https://www.firefox.com", id = "2")
+ )
+ ),
+ middleware = listOf(middleware)
+ )
+ val engineSession1: EngineSession = mock()
+ val engineSession2: EngineSession = mock()
+
+ store.dispatch(TabListAction.SelectTabAction("1")).joinBlocking()
+
+ assertEquals("", middleware.previousHighestPriorityTabId)
+
+ store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking()
+
+ assertEquals("1", middleware.previousHighestPriorityTabId)
+ verify(engineSession1).updateSessionPriority(HIGH)
+
+ store.dispatch(TabListAction.SelectTabAction("2")).joinBlocking()
+
+ assertEquals("1", middleware.previousHighestPriorityTabId)
+
+ store.dispatch(EngineAction.LinkEngineSessionAction("2", engineSession2)).joinBlocking()
+
+ assertEquals("2", middleware.previousHighestPriorityTabId)
+ verify(engineSession1).updateSessionPriority(DEFAULT)
+ verify(engineSession2).updateSessionPriority(HIGH)
+ }
+
+ @Test
+ fun `GIVEN no linked tab WHEN SelectTabAction THEN no changes in priority show happened`() {
+ val middleware = SessionPrioritizationMiddleware()
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "1"),
+ createTab("https://www.firefox.com", id = "2")
+ )
+ ),
+ middleware = listOf(middleware)
+ )
+
+ store.dispatch(TabListAction.SelectTabAction("1")).joinBlocking()
+
+ assertEquals("", middleware.previousHighestPriorityTabId)
+ }
+}
diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SuspendMiddlewareTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SuspendMiddlewareTest.kt
index 263ada2098f..7e9dd9931a8 100644
--- a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SuspendMiddlewareTest.kt
+++ b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SuspendMiddlewareTest.kt
@@ -5,9 +5,6 @@
package mozilla.components.browser.state.engine.middleware
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.BrowserState
@@ -18,9 +15,11 @@ import mozilla.components.concept.engine.EngineSessionState
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.never
@@ -30,16 +29,13 @@ import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class SuspendMiddlewareTest {
- private val dispatcher = TestCoroutineDispatcher()
- private val scope = CoroutineScope(dispatcher)
-
- @After
- fun tearDown() {
- dispatcher.cleanupTestCoroutines()
- }
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
@Test
- fun `suspends engine session`() = runBlocking {
+ fun `suspends engine session`() = runTestOnMain {
val middleware = SuspendMiddleware(scope)
val tab = createTab("https://www.mozilla.org", id = "1")
@@ -57,7 +53,7 @@ class SuspendMiddlewareTest {
store.dispatch(EngineAction.SuspendEngineSessionAction(tab.id)).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(store.state.findTab(tab.id)?.engineState?.engineSession)
assertEquals(state, store.state.findTab(tab.id)?.engineState?.engineSessionState)
@@ -122,7 +118,7 @@ class SuspendMiddlewareTest {
suspendStore.waitUntilIdle()
killStore.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(suspendStore.state.findTab(tab.id)?.engineState?.engineSession)
assertEquals(state, suspendStore.state.findTab(tab.id)?.engineState?.engineSessionState)
diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TabsRemovedMiddlewareTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TabsRemovedMiddlewareTest.kt
index bef7b38f1f1..7ab9150ea35 100644
--- a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TabsRemovedMiddlewareTest.kt
+++ b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TabsRemovedMiddlewareTest.kt
@@ -5,9 +5,6 @@
package mozilla.components.browser.state.engine.middleware
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.CustomTabListAction
import mozilla.components.browser.state.action.EngineAction
@@ -25,9 +22,11 @@ import mozilla.components.lib.state.MiddlewareContext
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.never
@@ -35,17 +34,13 @@ import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class TabsRemovedMiddlewareTest {
-
- private val dispatcher = TestCoroutineDispatcher()
- private val scope = CoroutineScope(dispatcher)
-
- @After
- fun tearDown() {
- dispatcher.cleanupTestCoroutines()
- }
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
@Test
- fun `closes and unlinks engine session when tab is removed`() = runBlocking {
+ fun `closes and unlinks engine session when tab is removed`() = runTestOnMain {
val middleware = TabsRemovedMiddleware(scope)
val tab = createTab("https://www.mozilla.org", id = "1")
@@ -57,14 +52,14 @@ class TabsRemovedMiddlewareTest {
val engineSession = linkEngineSession(store, tab.id)
store.dispatch(TabListAction.RemoveTabAction(tab.id)).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(store.state.findTab(tab.id)?.engineState?.engineSession)
verify(engineSession).close()
}
@Test
- fun `closes and unlinks engine session when list of tabs are removed`() = runBlocking {
+ fun `closes and unlinks engine session when list of tabs are removed`() = runTestOnMain {
val middleware = TabsRemovedMiddleware(scope)
val tab1 = createTab("https://www.mozilla.org", id = "1", private = false)
@@ -82,7 +77,7 @@ class TabsRemovedMiddlewareTest {
store.dispatch(TabListAction.RemoveTabsAction(listOf(tab1.id, tab2.id))).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(store.state.findTab(tab1.id)?.engineState?.engineSession)
assertNull(store.state.findTab(tab2.id)?.engineState?.engineSession)
@@ -93,7 +88,7 @@ class TabsRemovedMiddlewareTest {
}
@Test
- fun `closes and unlinks engine session when all normal tabs are removed`() = runBlocking {
+ fun `closes and unlinks engine session when all normal tabs are removed`() = runTestOnMain {
val middleware = TabsRemovedMiddleware(scope)
val tab1 = createTab("https://www.mozilla.org", id = "1", private = false)
@@ -110,7 +105,7 @@ class TabsRemovedMiddlewareTest {
store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(store.state.findTab(tab1.id)?.engineState?.engineSession)
assertNull(store.state.findTab(tab2.id)?.engineState?.engineSession)
@@ -121,7 +116,7 @@ class TabsRemovedMiddlewareTest {
}
@Test
- fun `closes and unlinks engine session when all private tabs are removed`() = runBlocking {
+ fun `closes and unlinks engine session when all private tabs are removed`() = runTestOnMain {
val middleware = TabsRemovedMiddleware(scope)
val tab1 = createTab("https://www.mozilla.org", id = "1", private = true)
@@ -138,7 +133,7 @@ class TabsRemovedMiddlewareTest {
store.dispatch(TabListAction.RemoveAllPrivateTabsAction).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(store.state.findTab(tab1.id)?.engineState?.engineSession)
assertNull(store.state.findTab(tab2.id)?.engineState?.engineSession)
@@ -149,7 +144,7 @@ class TabsRemovedMiddlewareTest {
}
@Test
- fun `closes and unlinks engine session when all tabs are removed`() = runBlocking {
+ fun `closes and unlinks engine session when all tabs are removed`() = runTestOnMain {
val middleware = TabsRemovedMiddleware(scope)
val tab1 = createTab("https://www.mozilla.org", id = "1", private = true)
@@ -166,7 +161,7 @@ class TabsRemovedMiddlewareTest {
store.dispatch(TabListAction.RemoveAllTabsAction()).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(store.state.findTab(tab1.id)?.engineState?.engineSession)
assertNull(store.state.findTab(tab2.id)?.engineState?.engineSession)
@@ -177,7 +172,7 @@ class TabsRemovedMiddlewareTest {
}
@Test
- fun `closes and unlinks engine session when custom tab is removed`() = runBlocking {
+ fun `closes and unlinks engine session when custom tab is removed`() = runTestOnMain {
val middleware = TabsRemovedMiddleware(scope)
val tab = createCustomTab("https://www.mozilla.org", id = "1")
@@ -189,14 +184,14 @@ class TabsRemovedMiddlewareTest {
val engineSession = linkEngineSession(store, tab.id)
store.dispatch(CustomTabListAction.RemoveCustomTabAction(tab.id)).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(store.state.findTab(tab.id)?.engineState?.engineSession)
verify(engineSession).close()
}
@Test
- fun `closes and unlinks engine session when all custom tabs are removed`() = runBlocking {
+ fun `closes and unlinks engine session when all custom tabs are removed`() = runTestOnMain {
val middleware = TabsRemovedMiddleware(scope)
val tab1 = createCustomTab("https://www.mozilla.org", id = "1")
@@ -213,7 +208,7 @@ class TabsRemovedMiddlewareTest {
store.dispatch(CustomTabListAction.RemoveAllCustomTabsAction).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(store.state.findCustomTab(tab1.id)?.engineState?.engineSession)
assertNull(store.state.findCustomTab(tab2.id)?.engineState?.engineSession)
diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TrimMemoryMiddlewareTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TrimMemoryMiddlewareTest.kt
index 60428efc03c..04c11334312 100644
--- a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TrimMemoryMiddlewareTest.kt
+++ b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TrimMemoryMiddlewareTest.kt
@@ -5,8 +5,6 @@
package mozilla.components.browser.state.engine.middleware
import android.content.ComponentCallbacks2
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.state.action.SystemAction
import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.browser.state.selector.findTab
@@ -20,14 +18,21 @@ import mozilla.components.concept.engine.EngineSessionState
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
class TrimMemoryMiddlewareTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
+
private lateinit var engineSessionReddit: EngineSession
private lateinit var engineSessionTheVerge: EngineSession
private lateinit var engineSessionTwitch: EngineSession
@@ -37,8 +42,6 @@ class TrimMemoryMiddlewareTest {
private lateinit var engineSessionFacebook: EngineSession
private lateinit var store: BrowserStore
- private val dispatcher = TestCoroutineDispatcher()
- private val scope = CoroutineScope(dispatcher)
private lateinit var engineSessionStateReddit: EngineSessionState
private lateinit var engineSessionStateTheVerge: EngineSessionState
@@ -132,7 +135,7 @@ class TrimMemoryMiddlewareTest {
).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.state.findTab("theverge")!!.engineState.apply {
assertNotNull(engineSession)
@@ -196,7 +199,7 @@ class TrimMemoryMiddlewareTest {
).joinBlocking()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.state.findTab("theverge")!!.engineState.apply {
assertNull(engineSession)
diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/state/SearchStateTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/state/SearchStateTest.kt
index 06f19a50e5c..ceacea86f68 100644
--- a/components/browser/state/src/test/java/mozilla/components/browser/state/state/SearchStateTest.kt
+++ b/components/browser/state/src/test/java/mozilla/components/browser/state/state/SearchStateTest.kt
@@ -26,6 +26,9 @@ class SearchStateTest {
SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM)
),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION),
+ ),
additionalSearchEngines = listOf(
SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL)
),
@@ -60,6 +63,9 @@ class SearchStateTest {
SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM)
),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION),
+ ),
additionalSearchEngines = listOf(
SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL)
),
@@ -94,6 +100,9 @@ class SearchStateTest {
SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM)
),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION),
+ ),
additionalSearchEngines = listOf(
SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL)
),
@@ -128,6 +137,9 @@ class SearchStateTest {
SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM)
),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION),
+ ),
additionalSearchEngines = listOf(
SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL)
),
@@ -168,6 +180,9 @@ class SearchStateTest {
SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM)
),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION),
+ ),
additionalSearchEngines = listOf(
SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL)
),
@@ -184,7 +199,7 @@ class SearchStateTest {
)
val searchEngines = state.searchEngines
- assertEquals(6, searchEngines.size)
+ assertEquals(7, searchEngines.size)
assertEquals("engine-a", searchEngines[0].id)
assertEquals("engine-b", searchEngines[1].id)
assertEquals("engine-c", searchEngines[2].id)
@@ -206,6 +221,9 @@ class SearchStateTest {
SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM)
),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION),
+ ),
additionalSearchEngines = listOf(
SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL)
),
diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreTest.kt
index 8b294e2cd6c..20d71d417ef 100644
--- a/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreTest.kt
+++ b/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreTest.kt
@@ -4,7 +4,7 @@
package mozilla.components.browser.state.store
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.InitAction
import mozilla.components.browser.state.action.RestoreCompleteAction
@@ -50,7 +50,7 @@ class BrowserStoreTest {
}
@Test
- fun `Adding a tab`() = runBlocking {
+ fun `Adding a tab`() = runTest {
val store = BrowserStore()
assertEquals(0, store.state.tabs.size)
diff --git a/components/browser/state/src/test/resources/robolectric.properties b/components/browser/state/src/test/resources/robolectric.properties
deleted file mode 100644
index 89a6c8b4c2e..00000000000
--- a/components/browser/state/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1 +0,0 @@
-sdk=28
\ No newline at end of file
diff --git a/components/browser/storage-sync/build.gradle b/components/browser/storage-sync/build.gradle
index 8766b3eede6..49e793c5555 100644
--- a/components/browser/storage-sync/build.gradle
+++ b/components/browser/storage-sync/build.gradle
@@ -46,6 +46,7 @@ dependencies {
testImplementation Dependencies.androidx_test_junit
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
+ testImplementation Dependencies.testing_coroutines
testImplementation Dependencies.mozilla_places
testImplementation Dependencies.mozilla_remote_tabs
diff --git a/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorage.kt b/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorage.kt
index 54594f30178..98521f3a5b3 100644
--- a/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorage.kt
+++ b/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorage.kt
@@ -156,7 +156,9 @@ open class PlacesHistoryStorage(
*/
override suspend fun deleteEverything() {
withContext(writeScope.coroutineContext) {
- places.writer().deleteEverything()
+ handlePlacesExceptions("deleteEverything") {
+ places.writer().deleteEverything()
+ }
}
}
diff --git a/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/RemoteTabsStorage.kt b/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/RemoteTabsStorage.kt
index 1186bbdaa13..e70bbff3624 100644
--- a/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/RemoteTabsStorage.kt
+++ b/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/RemoteTabsStorage.kt
@@ -4,6 +4,7 @@
package mozilla.components.browser.storage.sync
+import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancelChildren
@@ -15,16 +16,20 @@ import mozilla.components.concept.sync.Device
import mozilla.components.concept.sync.SyncableStore
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.utils.logElapsedTime
+import java.io.File
import mozilla.appservices.remotetabs.InternalException as RemoteTabProviderException
import mozilla.appservices.remotetabs.TabsStore as RemoteTabsProvider
+private const val TABS_DB_NAME = "tabs.sqlite"
+
/**
* An interface which defines read/write methods for remote tabs data.
*/
open class RemoteTabsStorage(
+ private val context: Context,
private val crashReporter: CrashReporting? = null
) : Storage, SyncableStore {
- internal val api by lazy { RemoteTabsProvider() }
+ internal val api by lazy { RemoteTabsProvider(File(context.filesDir, TABS_DB_NAME).canonicalPath) }
private val scope by lazy { CoroutineScope(Dispatchers.IO) }
internal val logger = Logger("RemoteTabsStorage")
diff --git a/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesBookmarksStorageTest.kt b/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesBookmarksStorageTest.kt
index 4e18bb4a535..9d793cf5452 100644
--- a/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesBookmarksStorageTest.kt
+++ b/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesBookmarksStorageTest.kt
@@ -5,13 +5,15 @@
package mozilla.components.browser.storage.sync
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.appservices.places.BookmarkRoot
import mozilla.appservices.places.uniffi.PlacesException
import mozilla.components.concept.storage.BookmarkInfo
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
@@ -19,28 +21,33 @@ import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.TimeUnit
+@ExperimentalCoroutinesApi // for runTestOnMain
@RunWith(AndroidJUnit4::class)
class PlacesBookmarksStorageTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
private lateinit var bookmarks: PlacesBookmarksStorage
@Before
- fun setup() = runBlocking {
+ fun setup() = runTestOnMain {
bookmarks = PlacesBookmarksStorage(testContext)
// There's a database on disk which needs to be cleaned up between tests.
bookmarks.writer.deleteEverything()
}
@After
- fun cleanup() = runBlocking {
+ fun cleanup() = runTestOnMain {
bookmarks.cleanup()
}
@Test
- fun `get bookmarks tree by root, recursive or not`() = runBlocking {
+ fun `get bookmarks tree by root, recursive or not`() = runTestOnMain {
val tree = bookmarks.getTree(BookmarkRoot.Root.id)!!
assertEquals(BookmarkRoot.Root.id, tree.guid)
assertNotNull(tree.children)
@@ -80,7 +87,7 @@ class PlacesBookmarksStorageTest {
}
@Test
- fun `bookmarks APIs smoke testing - basic operations`() = runBlocking {
+ fun `bookmarks APIs smoke testing - basic operations`() = runTestOnMain {
val url = "http://www.mozilla.org"
assertEquals(emptyList(), bookmarks.getBookmarksWithUrl(url))
@@ -204,7 +211,7 @@ class PlacesBookmarksStorageTest {
}
@Test
- fun `bookmarks import v34 populated`() = runBlocking {
+ fun `bookmarks import v34 populated`() = runTestOnMain {
val path = getTestPath("databases/history-v34.db").absolutePath
// Need to import history first before we import bookmarks.
@@ -244,7 +251,7 @@ class PlacesBookmarksStorageTest {
}
@Test
- fun `bookmarks import v38 populated`() = runBlocking {
+ fun `bookmarks import v38 populated`() = runTestOnMain {
val path = getTestPath("databases/populated-v38.db").absolutePath
// Need to import history first before we import bookmarks.
@@ -308,7 +315,7 @@ class PlacesBookmarksStorageTest {
}
@Test
- fun `bookmarks import v39 populated`() = runBlocking {
+ fun `bookmarks import v39 populated`() = runTestOnMain {
val path = getTestPath("databases/populated-v39.db").absolutePath
// Need to import history first before we import bookmarks.
@@ -369,7 +376,7 @@ class PlacesBookmarksStorageTest {
}
@Test
- fun `bookmarks pinned sites read v39`() = runBlocking {
+ fun `bookmarks pinned sites read v39`() = runTestOnMain {
val path = getTestPath("databases/pinnedSites-v39.db").absolutePath
with(bookmarks.readPinnedSitesFromFennec(path)) {
@@ -392,7 +399,7 @@ class PlacesBookmarksStorageTest {
}
@Test
- fun `bookmarks pinned sites read empty v39`() = runBlocking {
+ fun `bookmarks pinned sites read empty v39`() = runTestOnMain {
val path = getTestPath("databases/populated-v39.db").absolutePath
assertEquals(0, bookmarks.readPinnedSitesFromFennec(path).size)
diff --git a/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt b/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt
index 4080f06e0bc..656e9a17423 100644
--- a/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt
+++ b/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt
@@ -5,7 +5,8 @@
package mozilla.components.browser.storage.sync
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
import mozilla.appservices.places.PlacesReaderConnection
import mozilla.appservices.places.PlacesWriterConnection
import mozilla.appservices.places.uniffi.PlacesException
@@ -23,6 +24,8 @@ import mozilla.components.concept.sync.SyncAuthInfo
import mozilla.components.concept.sync.SyncStatus
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.json.JSONObject
import org.junit.After
import org.junit.Assert.assertEquals
@@ -32,28 +35,33 @@ import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
+@ExperimentalCoroutinesApi // for runTestOnMain
@RunWith(AndroidJUnit4::class)
class PlacesHistoryStorageTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
private lateinit var history: PlacesHistoryStorage
@Before
- fun setup() = runBlocking {
+ fun setup() = runTestOnMain {
history = PlacesHistoryStorage(testContext)
// There's a database on disk which needs to be cleaned up between tests.
history.deleteEverything()
}
@After
- fun cleanup() = runBlocking {
+ fun cleanup() = runTestOnMain {
history.cleanup()
}
@Test
- fun `storage allows recording and querying visits of different types`() = runBlocking {
+ fun `storage allows recording and querying visits of different types`() = runTestOnMain {
history.recordVisit("http://www.firefox.com/1", PageVisit(VisitType.LINK))
history.recordVisit("http://www.firefox.com/2", PageVisit(VisitType.RELOAD))
history.recordVisit("http://www.firefox.com/3", PageVisit(VisitType.TYPED))
@@ -141,7 +149,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `storage passes through recordObservation calls`() = runBlocking {
+ fun `storage passes through recordObservation calls`() = runTestOnMain {
history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.LINK))
history.recordObservation("http://www.mozilla.org", PageObservation(title = "Mozilla"))
@@ -159,7 +167,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `store can be used to query top frecent site information`() = runBlocking {
+ fun `store can be used to query top frecent site information`() = runTestOnMain {
val toAdd = listOf(
"https://www.example.com/123",
"https://www.example.com/123",
@@ -223,7 +231,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `store can be used to query detailed visit information`() = runBlocking {
+ fun `store can be used to query detailed visit information`() = runTestOnMain {
history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.LINK))
history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.RELOAD))
history.recordObservation(
@@ -254,7 +262,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `store can be used to record and retrieve history via webview-style callbacks`() = runBlocking {
+ fun `store can be used to record and retrieve history via webview-style callbacks`() = runTestOnMain {
// Empty.
assertEquals(0, history.getVisited().size)
@@ -281,7 +289,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `store can be used to record and retrieve history via gecko-style callbacks`() = runBlocking {
+ fun `store can be used to record and retrieve history via gecko-style callbacks`() = runTestOnMain {
assertEquals(0, history.getVisited(listOf()).size)
// Regular visits are tracked
@@ -304,7 +312,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `store can be used to track page meta information - title and previewImageUrl changes`() = runBlocking {
+ fun `store can be used to track page meta information - title and previewImageUrl changes`() = runTestOnMain {
// Title and previewImageUrl changes are recorded.
history.recordVisit("https://www.wikipedia.org", PageVisit(VisitType.TYPED))
history.recordObservation(
@@ -338,7 +346,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `store can provide suggestions`() = runBlocking {
+ fun `store can provide suggestions`() = runTestOnMain {
assertEquals(0, history.getSuggestions("Mozilla", 100).size)
history.recordVisit("http://www.firefox.com", PageVisit(VisitType.LINK))
@@ -388,7 +396,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `store can provide autocomplete suggestions`() = runBlocking {
+ fun `store can provide autocomplete suggestions`() = runTestOnMain {
assertNull(history.getAutocompleteSuggestion("moz"))
history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.LINK))
@@ -445,7 +453,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `store ignores url parse exceptions during record operations`() = runBlocking {
+ fun `store ignores url parse exceptions during record operations`() = runTestOnMain {
// These aren't valid URIs, and if we're not explicitly ignoring exceptions from the underlying
// storage layer, these calls will throw.
history.recordVisit("mozilla.org", PageVisit(VisitType.LINK))
@@ -453,7 +461,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `store can delete everything`() = runBlocking {
+ fun `store can delete everything`() = runTestOnMain {
history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.TYPED))
history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.DOWNLOAD))
history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.BOOKMARK))
@@ -473,7 +481,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `store can delete by url`() = runBlocking {
+ fun `store can delete by url`() = runTestOnMain {
history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.TYPED))
history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.DOWNLOAD))
history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.BOOKMARK))
@@ -497,7 +505,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `store can delete by 'since'`() = runBlocking {
+ fun `store can delete by 'since'`() = runTestOnMain {
history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.TYPED))
history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.DOWNLOAD))
history.recordVisit("http://www.mozilla.org", PageVisit(VisitType.BOOKMARK))
@@ -508,28 +516,21 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `store can delete by 'range'`() {
- runBlocking {
- history.recordVisit("http://www.mozilla.org/1", PageVisit(VisitType.TYPED))
- Thread.sleep(10)
- history.recordVisit("http://www.mozilla.org/2", PageVisit(VisitType.DOWNLOAD))
- Thread.sleep(10)
- history.recordVisit("http://www.mozilla.org/3", PageVisit(VisitType.BOOKMARK))
- }
+ fun `store can delete by 'range'`() = runTestOnMain {
+ history.recordVisit("http://www.mozilla.org/1", PageVisit(VisitType.TYPED))
+ advanceUntilIdle()
+ history.recordVisit("http://www.mozilla.org/2", PageVisit(VisitType.DOWNLOAD))
+ advanceUntilIdle()
+ history.recordVisit("http://www.mozilla.org/3", PageVisit(VisitType.BOOKMARK))
- val ts = runBlocking {
- val visits = history.getDetailedVisits(0, Long.MAX_VALUE)
+ var visits = history.getDetailedVisits(0, Long.MAX_VALUE)
+ assertEquals(3, visits.size)
+ val ts = visits[1].visitTime
- assertEquals(3, visits.size)
- visits[1].visitTime
- }
+ history.deleteVisitsBetween(ts - 1, ts + 1)
+
+ visits = history.getDetailedVisits(0, Long.MAX_VALUE)
- runBlocking {
- history.deleteVisitsBetween(ts - 1, ts + 1)
- }
- val visits = runBlocking {
- history.getDetailedVisits(0, Long.MAX_VALUE)
- }
assertEquals(2, visits.size)
assertEquals("http://www.mozilla.org/1", visits[0].url)
@@ -537,41 +538,28 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `store can delete visit by 'url' and 'timestamp'`() {
- runBlocking {
- history.recordVisit("http://www.mozilla.org/1", PageVisit(VisitType.TYPED))
- Thread.sleep(10)
- history.recordVisit("http://www.mozilla.org/2", PageVisit(VisitType.DOWNLOAD))
- Thread.sleep(10)
- history.recordVisit("http://www.mozilla.org/3", PageVisit(VisitType.BOOKMARK))
- }
-
- val ts = runBlocking {
- val visits = history.getDetailedVisits(0, Long.MAX_VALUE)
+ fun `store can delete visit by 'url' and 'timestamp'`() = runTestOnMain {
+ history.recordVisit("http://www.mozilla.org/1", PageVisit(VisitType.TYPED))
+ Thread.sleep(10)
+ history.recordVisit("http://www.mozilla.org/2", PageVisit(VisitType.DOWNLOAD))
+ Thread.sleep(10)
+ history.recordVisit("http://www.mozilla.org/3", PageVisit(VisitType.BOOKMARK))
- assertEquals(3, visits.size)
- visits[1].visitTime
- }
+ var visits = history.getDetailedVisits(0, Long.MAX_VALUE)
+ assertEquals(3, visits.size)
+ val ts = visits[1].visitTime
- runBlocking {
- history.deleteVisit("http://www.mozilla.org/4", 111)
- // There are no visits for this url, delete is a no-op.
- assertEquals(3, history.getDetailedVisits(0, Long.MAX_VALUE).size)
- }
+ history.deleteVisit("http://www.mozilla.org/4", 111)
+ // There are no visits for this url, delete is a no-op.
+ assertEquals(3, history.getDetailedVisits(0, Long.MAX_VALUE).size)
- runBlocking {
- history.deleteVisit("http://www.mozilla.org/1", ts)
- // There is no such visit for this url, delete is a no-op.
- assertEquals(3, history.getDetailedVisits(0, Long.MAX_VALUE).size)
- }
+ history.deleteVisit("http://www.mozilla.org/1", ts)
+ // There is no such visit for this url, delete is a no-op.
+ assertEquals(3, history.getDetailedVisits(0, Long.MAX_VALUE).size)
- runBlocking {
- history.deleteVisit("http://www.mozilla.org/2", ts)
- }
+ history.deleteVisit("http://www.mozilla.org/2", ts)
- val visits = runBlocking {
- history.getDetailedVisits(0, Long.MAX_VALUE)
- }
+ visits = history.getDetailedVisits(0, Long.MAX_VALUE)
assertEquals(2, visits.size)
assertEquals("http://www.mozilla.org/1", visits[0].url)
@@ -579,12 +567,12 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `can run maintanence on the store`() = runBlocking {
+ fun `can run maintanence on the store`() = runTestOnMain {
history.runMaintenance()
}
@Test
- fun `can run prune on the store`() = runBlocking {
+ fun `can run prune on the store`() = runTestOnMain {
// Empty.
history.prune()
history.recordVisit("http://www.mozilla.org/1", PageVisit(VisitType.TYPED))
@@ -602,7 +590,7 @@ class PlacesHistoryStorageTest {
internal class MockingPlacesHistoryStorage(override val places: Connection) : PlacesHistoryStorage(testContext)
@Test
- fun `storage passes through sync calls`() = runBlocking {
+ fun `storage passes through sync calls`() = runTestOnMain {
var passedAuthInfo: SyncAuthInfo? = null
val conn = object : Connection {
override fun reader(): PlacesReaderConnection {
@@ -659,7 +647,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `storage passes through sync OK results`() = runBlocking {
+ fun `storage passes through sync OK results`() = runTestOnMain {
val conn = object : Connection {
override fun reader(): PlacesReaderConnection {
fail()
@@ -705,7 +693,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `storage passes through sync exceptions`() = runBlocking {
+ fun `storage passes through sync exceptions`() = runTestOnMain {
// Can be any PlacesException
val exception = PlacesException.UrlParseFailed("test error")
val conn = object : Connection {
@@ -761,7 +749,7 @@ class PlacesHistoryStorageTest {
}
@Test(expected = PlacesException::class)
- fun `storage re-throws sync panics`() = runBlocking {
+ fun `storage re-throws sync panics`() = runTestOnMain {
val exception = PlacesException.UnexpectedPlacesException("test panic")
val conn = object : Connection {
override fun reader(): PlacesReaderConnection {
@@ -837,7 +825,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `history import v38 populated`() = runBlocking {
+ fun `history import v38 populated`() = runTestOnMain {
val path = getTestPath("databases/populated-v38.db").absolutePath
var visits = history.getDetailedVisits(0, Long.MAX_VALUE)
assertEquals(0, visits.size)
@@ -868,7 +856,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `history import v39 populated`() = runBlocking {
+ fun `history import v39 populated`() = runTestOnMain {
val path = getTestPath("databases/populated-v39.db").absolutePath
var visits = history.getDetailedVisits(0, Long.MAX_VALUE)
assertEquals(0, visits.size)
@@ -931,7 +919,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `history import v34 populated`() = runBlocking {
+ fun `history import v34 populated`() = runTestOnMain {
val path = getTestPath("databases/history-v34.db").absolutePath
var visits = history.getDetailedVisits(0, Long.MAX_VALUE)
assertEquals(0, visits.size)
@@ -992,7 +980,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `record and get latest history metadata by url`() = runBlocking {
+ fun `record and get latest history metadata by url`() = runTestOnMain {
val metaKey = HistoryMetadataKey(
url = "https://doc.rust-lang.org/std/macro.assert_eq.html",
searchTerm = "rust assert_eq",
@@ -1009,7 +997,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `get history query`() = runBlocking {
+ fun `get history query`() = runTestOnMain {
assertEquals(0, history.queryHistoryMetadata("keystore", 1).size)
val metaKey1 = HistoryMetadataKey(
@@ -1087,7 +1075,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `get history metadata between`() = runBlocking {
+ fun `get history metadata between`() = runTestOnMain {
assertEquals(0, history.getHistoryMetadataBetween(-1, 0).size)
assertEquals(0, history.getHistoryMetadataBetween(0, Long.MAX_VALUE).size)
assertEquals(0, history.getHistoryMetadataBetween(Long.MAX_VALUE, Long.MIN_VALUE).size)
@@ -1140,7 +1128,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `get history metadata since`() = runBlocking {
+ fun `get history metadata since`() = runTestOnMain {
val beginning = System.currentTimeMillis()
assertEquals(0, history.getHistoryMetadataSince(-1).size)
@@ -1198,7 +1186,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `delete history metadata by search term`() = runBlocking {
+ fun `delete history metadata by search term`() = runTestOnMain {
// Able to operate against an empty db
history.deleteHistoryMetadata("test")
history.deleteHistoryMetadata("")
@@ -1312,7 +1300,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `safe read from places`() = runBlocking {
+ fun `safe read from places`() = runTestOnMain {
val result = history.handlePlacesExceptions("test", default = emptyList()) {
// Can be any PlacesException error
throw PlacesException.PlacesConnectionBusy("test")
@@ -1344,7 +1332,7 @@ class PlacesHistoryStorageTest {
}
@Test
- fun `delete history metadata by url`() = runBlocking {
+ fun `delete history metadata by url`() = runTestOnMain {
// Able to operate against an empty db
history.deleteHistoryMetadataForUrl("https://mozilla.org")
history.deleteHistoryMetadataForUrl("")
diff --git a/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/RemoteTabsStorageTest.kt b/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/RemoteTabsStorageTest.kt
index 6fbe31ac985..768c56c0355 100644
--- a/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/RemoteTabsStorageTest.kt
+++ b/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/RemoteTabsStorageTest.kt
@@ -6,13 +6,14 @@ package mozilla.components.browser.storage.sync
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.appservices.remotetabs.ClientRemoteTabs
import mozilla.appservices.remotetabs.DeviceType
import mozilla.appservices.remotetabs.RemoteTab
import mozilla.components.concept.base.crash.CrashReporting
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
@@ -36,7 +37,7 @@ class RemoteTabsStorageTest {
@Before
fun setup() {
crashReporter = mock()
- remoteTabs = spy(RemoteTabsStorage(crashReporter))
+ remoteTabs = spy(RemoteTabsStorage(testContext, crashReporter))
apiMock = mock(RemoteTabsProvider::class.java)
`when`(remoteTabs.api).thenReturn(apiMock)
}
@@ -47,7 +48,7 @@ class RemoteTabsStorageTest {
}
@Test
- fun `store() translates tabs to rust format`() = runBlocking {
+ fun `store() translates tabs to rust format`() = runTest {
remoteTabs.store(
listOf(
Tab(
@@ -85,7 +86,7 @@ class RemoteTabsStorageTest {
}
@Test
- fun `getAll() translates tabs to our format`() = runBlocking {
+ fun `getAll() translates tabs to our format`() = runTest {
`when`(apiMock.getAll()).thenReturn(
listOf(
ClientRemoteTabs(
@@ -136,7 +137,7 @@ class RemoteTabsStorageTest {
}
@Test
- fun `exceptions from getAll are propagated to the crash reporter`() = runBlocking {
+ fun `exceptions from getAll are propagated to the crash reporter`() = runTest {
val throwable = RemoteTabProviderException("test")
`when`(apiMock.getAll()).thenAnswer { throw throwable }
diff --git a/components/browser/storage-sync/src/test/resources/robolectric.properties b/components/browser/storage-sync/src/test/resources/robolectric.properties
deleted file mode 100644
index 89a6c8b4c2e..00000000000
--- a/components/browser/storage-sync/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1 +0,0 @@
-sdk=28
\ No newline at end of file
diff --git a/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/SelectableTabViewHolder.kt b/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/SelectableTabViewHolder.kt
new file mode 100644
index 00000000000..8a9aa7b9529
--- /dev/null
+++ b/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/components/browser/tabstray/src/test/resources/robolectric.properties b/components/browser/tabstray/src/test/resources/robolectric.properties
deleted file mode 100644
index 89a6c8b4c2e..00000000000
--- a/components/browser/tabstray/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1 +0,0 @@
-sdk=28
\ No newline at end of file
diff --git a/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoader.kt b/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoader.kt
index fffc3af269d..72de8dc9384 100644
--- a/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoader.kt
+++ b/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoader.kt
@@ -46,8 +46,6 @@ class ThumbnailLoader(private val storage: ThumbnailStorage) : ImageLoader {
val existingJob = view.get()?.getTag(R.id.mozac_browser_thumbnails_tag_job) as? Job
existingJob?.cancel()
- view.get()?.setImageDrawable(placeholder)
-
// Create a loading job
val deferredThumbnail = storage.loadThumbnail(request)
@@ -58,7 +56,11 @@ class ThumbnailLoader(private val storage: ThumbnailStorage) : ImageLoader {
try {
val thumbnail = deferredThumbnail.await()
- view.get()?.setImageBitmap(thumbnail)
+ if (thumbnail != null) {
+ view.get()?.setImageBitmap(thumbnail)
+ } else {
+ view.get()?.setImageDrawable(placeholder)
+ }
} catch (e: CancellationException) {
view.get()?.setImageDrawable(error)
} finally {
diff --git a/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/BrowserThumbnailsTest.kt b/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/BrowserThumbnailsTest.kt
index 7509b423515..929ba0010fa 100644
--- a/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/BrowserThumbnailsTest.kt
+++ b/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/BrowserThumbnailsTest.kt
@@ -6,10 +6,6 @@ package mozilla.components.browser.thumbnails
import android.graphics.Bitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.createTab
@@ -19,8 +15,9 @@ import mozilla.components.support.test.any
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.`when`
@@ -33,7 +30,8 @@ import org.mockito.Mockito.verifyNoMoreInteractions
@RunWith(AndroidJUnit4::class)
class BrowserThumbnailsTest {
- private val testDispatcher = TestCoroutineDispatcher()
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
private lateinit var store: BrowserStore
private lateinit var engineView: EngineView
@@ -42,7 +40,6 @@ class BrowserThumbnailsTest {
@Before
fun setup() {
- Dispatchers.setMain(testDispatcher)
store = spy(
BrowserStore(
BrowserState(
@@ -57,12 +54,6 @@ class BrowserThumbnailsTest {
thumbnails = BrowserThumbnails(testContext, engineView, store)
}
- @After
- fun tearDown() {
- Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
- }
-
@Test
fun `do not capture thumbnail when feature is stopped and a site finishes loading`() {
thumbnails.start()
diff --git a/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoaderTest.kt b/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoaderTest.kt
index 4cbf9988253..be3781d49d3 100644
--- a/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoaderTest.kt
+++ b/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoaderTest.kt
@@ -44,7 +44,6 @@ class ThumbnailLoaderTest {
loader.loadIntoView(view, request)
- verify(view).setImageDrawable(null)
verify(view).addOnAttachStateChangeListener(any())
verify(view).setTag(eq(R.id.mozac_browser_thumbnails_tag_job), any())
verify(view, never()).setImageBitmap(any())
@@ -70,8 +69,6 @@ class ThumbnailLoaderTest {
loader.loadIntoView(view, request, placeholder = placeholder, error = error)
- verify(view).setImageDrawable(placeholder)
-
result.cancel()
verify(view).setImageDrawable(error)
diff --git a/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorageTest.kt b/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorageTest.kt
index 234fd9fdec8..07e35e6ae58 100644
--- a/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorageTest.kt
+++ b/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorageTest.kt
@@ -7,17 +7,18 @@ package mozilla.components.browser.thumbnails.storage
import android.graphics.Bitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.concept.base.images.ImageLoadRequest
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.`when`
@@ -26,7 +27,9 @@ import org.mockito.Mockito.spy
@RunWith(AndroidJUnit4::class)
class ThumbnailStorageTest {
- private val testDispatcher = TestCoroutineDispatcher()
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val testDispatcher = coroutinesTestRule.testDispatcher
@Before
@After
@@ -35,7 +38,7 @@ class ThumbnailStorageTest {
}
@Test
- fun `clearThumbnails`() = runBlocking {
+ fun `clearThumbnails`() = runTestOnMain {
val bitmap: Bitmap = mock()
val thumbnailStorage = spy(ThumbnailStorage(testContext, testDispatcher))
@@ -54,7 +57,7 @@ class ThumbnailStorageTest {
}
@Test
- fun `deleteThumbnail`() = runBlocking {
+ fun `deleteThumbnail`() = runTestOnMain {
val request = "test-tab1"
val bitmap: Bitmap = mock()
val thumbnailStorage = spy(ThumbnailStorage(testContext, testDispatcher))
@@ -69,7 +72,7 @@ class ThumbnailStorageTest {
}
@Test
- fun `saveThumbnail`() = runBlocking {
+ fun `saveThumbnail`() = runTestOnMain {
val request = ImageLoadRequest("test-tab1", 100)
val bitmap: Bitmap = mock()
val thumbnailStorage = spy(ThumbnailStorage(testContext))
@@ -83,7 +86,7 @@ class ThumbnailStorageTest {
}
@Test
- fun `loadThumbnail`() = runBlocking {
+ fun `loadThumbnail`() = runTestOnMain {
val request = ImageLoadRequest("test-tab1", 100)
val bitmap: Bitmap = mock()
val thumbnailStorage = spy(ThumbnailStorage(testContext, testDispatcher))
diff --git a/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCacheTest.kt b/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCacheTest.kt
index eeb830a7be4..236df232022 100644
--- a/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCacheTest.kt
+++ b/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCacheTest.kt
@@ -18,6 +18,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mockito.`when`
+import org.robolectric.annotation.Config
import java.io.IOException
import java.io.OutputStream
@@ -25,15 +26,40 @@ import java.io.OutputStream
class ThumbnailDiskCacheTest {
@Test
- fun `Writing and reading bitmap bytes`() {
+ fun `Writing and reading bitmap bytes for sdk higher than 29`() {
val cache = ThumbnailDiskCache()
val request = ImageLoadRequest("123", 100)
val bitmap: Bitmap = mock()
`when`(bitmap.compress(any(), ArgumentMatchers.anyInt(), any())).thenAnswer {
Assert.assertEquals(
- @Suppress("DEPRECATION")
- // Deprecation will be handled in https://github.com/mozilla-mobile/android-components/issues/9555
+ Bitmap.CompressFormat.WEBP_LOSSY,
+ it.arguments[0] as Bitmap.CompressFormat
+ )
+ Assert.assertEquals(90, it.arguments[1] as Int) // Quality
+
+ val stream = it.arguments[2] as OutputStream
+ stream.write("Hello World".toByteArray())
+ true
+ }
+
+ cache.putThumbnailBitmap(testContext, request.id, bitmap)
+
+ val data = cache.getThumbnailData(testContext, request)
+ assertNotNull(data!!)
+ Assert.assertEquals("Hello World", String(data))
+ }
+
+ @Config(sdk = [29])
+ @Test
+ fun `Writing and reading bitmap bytes for sdk lower or equal to 29`() {
+ val cache = ThumbnailDiskCache()
+ val request = ImageLoadRequest("123", 100)
+
+ val bitmap: Bitmap = mock()
+ `when`(bitmap.compress(any(), ArgumentMatchers.anyInt(), any())).thenAnswer {
+ Assert.assertEquals(
+ @Suppress("DEPRECATION") // not deprecated in sdk 29
Bitmap.CompressFormat.WEBP,
it.arguments[0] as Bitmap.CompressFormat
)
diff --git a/components/browser/thumbnails/src/test/resources/robolectric.properties b/components/browser/thumbnails/src/test/resources/robolectric.properties
deleted file mode 100644
index 89a6c8b4c2e..00000000000
--- a/components/browser/thumbnails/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1 +0,0 @@
-sdk=28
\ No newline at end of file
diff --git a/components/browser/toolbar/build.gradle b/components/browser/toolbar/build.gradle
index 1a4fd7d85f4..a7be0de798c 100644
--- a/components/browser/toolbar/build.gradle
+++ b/components/browser/toolbar/build.gradle
@@ -47,6 +47,7 @@ dependencies {
testImplementation Dependencies.androidx_test_junit
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
+ testImplementation Dependencies.testing_coroutines
}
apply from: '../../../publish.gradle'
diff --git a/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt
index d49e4fc3f7f..648b59553b3 100644
--- a/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt
+++ b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt
@@ -75,7 +75,8 @@ class BrowserToolbar @JvmOverloads constructor(
defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr), Toolbar {
private var state: State = State.DISPLAY
- private var searchTerms: String = ""
+ @VisibleForTesting
+ internal var searchTerms: String = ""
private var urlCommitListener: ((String) -> Boolean)? = null
/**
@@ -219,11 +220,11 @@ class BrowserToolbar @JvmOverloads constructor(
}
override fun setSearchTerms(searchTerms: String) {
+ this.searchTerms = searchTerms.take(MAX_URI_LENGTH)
+
if (state == State.EDIT) {
- edit.editSuggestion(searchTerms)
+ edit.editSuggestion(this.searchTerms)
}
-
- this.searchTerms = searchTerms
}
override fun displayProgress(progress: Int) {
@@ -323,6 +324,13 @@ class BrowserToolbar @JvmOverloads constructor(
edit.addEditActionEnd(action)
}
+ /**
+ * Removes an action end of the URL in edit mode.
+ */
+ override fun removeEditActionEnd(action: Toolbar.Action) {
+ edit.removeEditActionEnd(action)
+ }
+
/**
* Switches to URL editing mode.
*/
diff --git a/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt
index b3b72fd92b9..30e1d903907 100644
--- a/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt
+++ b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt
@@ -27,6 +27,7 @@ import mozilla.components.browser.toolbar.R
import mozilla.components.browser.toolbar.internal.ActionContainer
import mozilla.components.concept.menu.MenuController
import mozilla.components.concept.toolbar.Toolbar
+import mozilla.components.support.ktx.android.content.isScreenReaderEnabled
/**
* Sub-component of the browser toolbar responsible for displaying the URL and related controls ("display mode").
@@ -157,19 +158,7 @@ class DisplayToolbar internal constructor(
origin = rootView.findViewById(R.id.mozac_browser_toolbar_origin_view).also {
it.toolbar = toolbar
},
- progress = rootView.findViewById(R.id.mozac_browser_toolbar_progress).apply {
- accessibilityDelegate = object : View.AccessibilityDelegate() {
- override fun onInitializeAccessibilityEvent(host: View?, event: AccessibilityEvent?) {
- super.onInitializeAccessibilityEvent(host, event)
- if (event?.eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
- // Populate the scroll event with the current progress.
- // See accessibility note in `updateProgress()`.
- event.scrollY = progress
- event.maxScrollY = max
- }
- }
- }
- },
+ progress = rootView.findViewById(R.id.mozac_browser_toolbar_progress),
highlight = rootView.findViewById(R.id.mozac_browser_toolbar_permission_indicator)
)
@@ -567,7 +556,14 @@ class DisplayToolbar internal constructor(
}
views.progress.progress = progress
- views.progress.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED)
+ val event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED).apply {
+ scrollY = progress
+ maxScrollY = views.progress.max
+ }
+
+ if (context.isScreenReaderEnabled) {
+ views.progress.parent.requestSendAccessibilityEvent(views.progress, event)
+ }
if (progress >= views.progress.max) {
// Loading is done, hide progress bar.
diff --git a/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/edit/EditToolbar.kt b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/edit/EditToolbar.kt
index bcbf9ac3c46..1748da51303 100644
--- a/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/edit/EditToolbar.kt
+++ b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/edit/EditToolbar.kt
@@ -248,6 +248,10 @@ class EditToolbar internal constructor(
views.editActionsEnd.addAction(action)
}
+ internal fun removeEditActionEnd(action: Toolbar.Action) {
+ views.editActionsEnd.removeAction(action)
+ }
+
/**
* Updates the text of the URL input field. Note: this does *not* affect the value of url itself
* and is only a visual change
diff --git a/components/browser/toolbar/src/main/res/values-ast/strings.xml b/components/browser/toolbar/src/main/res/values-ast/strings.xml
index efd50cb44c7..4edbea0c526 100644
--- a/components/browser/toolbar/src/main/res/values-ast/strings.xml
+++ b/components/browser/toolbar/src/main/res/values-ast/strings.xml
@@ -2,7 +2,7 @@
Menú
- Llimpiar
+ BorrarLa proteición antirrastrexu ta activada
@@ -14,5 +14,5 @@
Cargando
- Bloquióse parte del conteníu pol axuste de reproducción automática
+ L\'axuste de reproducción automática bloquió parte del conteníu
diff --git a/components/browser/toolbar/src/main/res/values-skr/strings.xml b/components/browser/toolbar/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..9a10e8b1c5e
--- /dev/null
+++ b/components/browser/toolbar/src/main/res/values-skr/strings.xml
@@ -0,0 +1,18 @@
+
+
+
+ مینیو
+ صاف کرو
+
+ سراغ کاری تحفظ چالو ہے
+
+ سراغ کاری تحفظ نے سُراغ رساں کوں بلاک کر ݙتا ہے
+
+ سراغ کاری تحفظ ایں سائٹ کیتے بند ہے
+
+ سائٹ ڄاݨکاری
+
+ لوڈ تھیندا پئے
+
+ کجھ مواد کوں آٹو پلے ترتیباں نال بلاک کر ݙتا ڳئے
+
diff --git a/components/browser/toolbar/src/main/res/values-yo/strings.xml b/components/browser/toolbar/src/main/res/values-yo/strings.xml
new file mode 100644
index 00000000000..6f46be12866
--- /dev/null
+++ b/components/browser/toolbar/src/main/res/values-yo/strings.xml
@@ -0,0 +1,18 @@
+
+
+
+ Mẹ́nù
+ Paárẹ́
+
+ Ìtọpinpin Ìdàábòbò wà ní títàn
+
+ Ìtọpinpin ìdàábòbò ti dènà atọpinpin
+
+ Ìtọpinpin ìdàábòbò ti di pípa fún ìkànnì yìí
+
+ Ìfitóniléti ìkànnì
+
+ Ó ń gbáradì
+
+ Àwọn àkòónú kan ti di dídénà ààtò ìfi-ara-ẹni-ṣisẹ́
+
diff --git a/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/AsyncFilterListenerTest.kt b/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/AsyncFilterListenerTest.kt
index 22d87bc5d42..56a289b940e 100644
--- a/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/AsyncFilterListenerTest.kt
+++ b/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/AsyncFilterListenerTest.kt
@@ -4,11 +4,12 @@
package mozilla.components.browser.toolbar
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.isActive
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.toolbar.AutocompleteDelegate
import mozilla.components.concept.toolbar.AutocompleteResult
import mozilla.components.support.test.mock
@@ -24,9 +25,10 @@ import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import java.util.concurrent.Executor
+@ExperimentalCoroutinesApi // for runTest
class AsyncFilterListenerTest {
@Test
- fun `filter listener cancels prior filter executions`() = runBlocking {
+ fun `filter listener cancels prior filter executions`() = runTest {
val urlView: AutocompleteView = mock()
val filter: suspend (String, AutocompleteDelegate) -> Unit = mock()
@@ -46,7 +48,7 @@ class AsyncFilterListenerTest {
}
@Test
- fun `filter delegate checks for cancellations before it runs, passes results to autocomplete view`() = runBlocking {
+ fun `filter delegate checks for cancellations before it runs, passes results to autocomplete view`() = runTest {
var filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
assertEquals("test", query)
delegate.applyAutocompleteResult(
@@ -132,7 +134,7 @@ class AsyncFilterListenerTest {
}
@Test
- fun `delegate discards stale results`() = runBlocking {
+ fun `delegate discards stale results`() = runTest {
val filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
assertEquals("test", query)
delegate.applyAutocompleteResult(
@@ -169,7 +171,7 @@ class AsyncFilterListenerTest {
}
@Test
- fun `delegate discards stale lack of results`() = runBlocking {
+ fun `delegate discards stale lack of results`() = runTest {
val filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
assertEquals("test", query)
delegate.noAutocompleteResult("test")
@@ -198,7 +200,7 @@ class AsyncFilterListenerTest {
}
@Test
- fun `delegate passes through non-stale lack of results`() = runBlocking {
+ fun `delegate passes through non-stale lack of results`() = runTest {
val filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
assertEquals("test", query)
delegate.noAutocompleteResult("test")
@@ -230,7 +232,7 @@ class AsyncFilterListenerTest {
}
@Test
- fun `delegate discards results if parent scope was cancelled`() = runBlocking {
+ fun `delegate discards results if parent scope was cancelled`() = runTest {
var preservedDelegate: AutocompleteDelegate? = null
val filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
@@ -291,7 +293,7 @@ class AsyncFilterListenerTest {
}
@Test
- fun `delegate discards lack of results if parent scope was cancelled`() = runBlocking {
+ fun `delegate discards lack of results if parent scope was cancelled`() = runTest {
var preservedDelegate: AutocompleteDelegate? = null
val filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
diff --git a/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt b/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt
index 3f186cd14ad..7ab70966ec5 100644
--- a/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt
+++ b/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt
@@ -50,7 +50,7 @@ import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.robolectric.Robolectric
-import org.robolectric.Shadows
+import org.robolectric.Shadows.shadowOf
@RunWith(AndroidJUnit4::class)
class BrowserToolbarTest {
@@ -152,6 +152,38 @@ class BrowserToolbarTest {
assertEquals("c".repeat(MAX_URI_LENGTH - 1), capturedValues[2])
}
+ @Test
+ fun `searchTerms is truncated in case it is greater than MAX_URI_LENGTH`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.edit = spy(toolbar.edit)
+ toolbar.editMode()
+
+ toolbar.setSearchTerms("a".repeat(MAX_URI_LENGTH + 1))
+
+ // Value was too long and should've been truncated
+ assertEquals(toolbar.searchTerms.length, MAX_URI_LENGTH)
+ verify(toolbar.edit).editSuggestion("a".repeat(MAX_URI_LENGTH))
+ }
+
+ @Test
+ fun `searchTerms is not truncated in case it is equal or less than MAX_URI_LENGTH`() {
+ val toolbar = BrowserToolbar(testContext)
+ toolbar.edit = spy(toolbar.edit)
+ toolbar.editMode()
+
+ toolbar.setSearchTerms("b".repeat(MAX_URI_LENGTH))
+
+ // Value should be the same as before
+ assertEquals(toolbar.searchTerms.length, MAX_URI_LENGTH)
+ verify(toolbar.edit).editSuggestion("b".repeat(MAX_URI_LENGTH))
+
+ toolbar.setSearchTerms("c".repeat(MAX_URI_LENGTH - 1))
+
+ // Value should be the same as before
+ assertEquals(toolbar.searchTerms.length, MAX_URI_LENGTH - 1)
+ verify(toolbar.edit).editSuggestion("c".repeat(MAX_URI_LENGTH - 1))
+ }
+
@Test
fun `last URL will be forwarded to edit toolbar when switching mode`() {
val toolbar = BrowserToolbar(testContext)
@@ -169,10 +201,12 @@ class BrowserToolbarTest {
fun `displayProgress will send accessibility events`() {
val toolbar = BrowserToolbar(testContext)
val root = mock(ViewParent::class.java)
- Shadows.shadowOf(toolbar).setMyParent(root)
+ shadowOf(toolbar).setMyParent(root)
`when`(root.requestSendAccessibilityEvent(any(), any())).thenReturn(false)
- Shadows.shadowOf(testContext.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager).setEnabled(true)
+ val shadowAccessibilityManager = shadowOf(testContext.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager)
+ shadowAccessibilityManager.setEnabled(true)
+ shadowAccessibilityManager.setTouchExplorationEnabled(true)
toolbar.displayProgress(10)
toolbar.displayProgress(50)
@@ -205,6 +239,31 @@ class BrowserToolbarTest {
assertEquals(100, captor.allValues[3].maxScrollY)
}
+ @Test
+ fun `displayProgress will not send send view scrolled accessibility events if touch exploration is disabled`() {
+ val toolbar = BrowserToolbar(testContext)
+ val root = mock(ViewParent::class.java)
+ shadowOf(toolbar).setMyParent(root)
+ `when`(root.requestSendAccessibilityEvent(any(), any())).thenReturn(false)
+
+ val shadowAccessibilityManager = shadowOf(testContext.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager)
+ shadowAccessibilityManager.setEnabled(true)
+ shadowAccessibilityManager.setTouchExplorationEnabled(false)
+
+ toolbar.displayProgress(10)
+ toolbar.displayProgress(50)
+ toolbar.displayProgress(100)
+
+ // make sure multiple calls to 100% does not trigger "loading" announcement
+ toolbar.displayProgress(100)
+
+ val captor = ArgumentCaptor.forClass(AccessibilityEvent::class.java)
+
+ verify(root, times(1)).requestSendAccessibilityEvent(any(), captor.capture())
+
+ assertEquals(AccessibilityEvent.TYPE_ANNOUNCEMENT, captor.allValues[0].eventType)
+ assertEquals(testContext.getString(R.string.mozac_browser_toolbar_progress_loading), captor.allValues[0].text[0])
+ }
@Test
fun `displayProgress will be forwarded to display toolbar`() {
val toolbar = BrowserToolbar(testContext)
@@ -465,6 +524,22 @@ class BrowserToolbarTest {
verify(edit).addEditActionEnd(action)
}
+ @Test
+ fun `WHEN removing action end THEN it will be forwarded to the edit toolbar`() {
+ val toolbar = BrowserToolbar(testContext)
+
+ val edit: EditToolbar = mock()
+ toolbar.edit = edit
+
+ val action = BrowserToolbar.Button(mock(), "QR code scanner") {
+ // Do nothing
+ }
+
+ toolbar.removeEditActionEnd(action)
+
+ verify(edit).removeEditActionEnd(action)
+ }
+
@Test
fun `cast to view`() {
// Given
diff --git a/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/edit/EditToolbarTest.kt b/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/edit/EditToolbarTest.kt
index 19ed8a46d12..9438360a1b8 100644
--- a/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/edit/EditToolbarTest.kt
+++ b/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/edit/EditToolbarTest.kt
@@ -7,7 +7,8 @@ package mozilla.components.browser.toolbar.edit
import android.view.KeyEvent
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.R
import mozilla.components.concept.toolbar.AutocompleteDelegate
@@ -27,6 +28,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class EditToolbarTest {
private fun createEditToolbar(): Pair {
@@ -39,7 +41,7 @@ class EditToolbarTest {
}
@Test
- fun `entered text is forwarded to async autocomplete filter`() {
+ fun `entered text is forwarded to async autocomplete filter`() = runTest {
val toolbar = BrowserToolbar(testContext)
toolbar.edit.views.url.onAttachedToWindow()
@@ -55,9 +57,7 @@ class EditToolbarTest {
// Autocomplete filter will be invoked on a worker thread.
// Serialize here for the sake of tests.
- runBlocking {
- latch.await()
- }
+ latch.await()
assertEquals("Hello", invokedWithParams!![0])
assertTrue(invokedWithParams!![1] is AutocompleteDelegate)
diff --git a/components/browser/toolbar/src/test/resources/robolectric.properties b/components/browser/toolbar/src/test/resources/robolectric.properties
deleted file mode 100644
index 89a6c8b4c2e..00000000000
--- a/components/browser/toolbar/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1 +0,0 @@
-sdk=28
\ No newline at end of file
diff --git a/components/compose/awesomebar/src/main/res/values-ban/strings.xml b/components/compose/awesomebar/src/main/res/values-ban/strings.xml
new file mode 100644
index 00000000000..5592ff756e9
--- /dev/null
+++ b/components/compose/awesomebar/src/main/res/values-ban/strings.xml
@@ -0,0 +1,5 @@
+
+
+
+ Cumpu miwah uah saran
+
diff --git a/components/compose/awesomebar/src/main/res/values-skr/strings.xml b/components/compose/awesomebar/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..10bff0878f0
--- /dev/null
+++ b/components/compose/awesomebar/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+
+
+
+ تجویز کوں قبول کرو تے تبدیلی کرو
+
diff --git a/components/compose/awesomebar/src/main/res/values-yo/strings.xml b/components/compose/awesomebar/src/main/res/values-yo/strings.xml
new file mode 100644
index 00000000000..9112ee08567
--- /dev/null
+++ b/components/compose/awesomebar/src/main/res/values-yo/strings.xml
@@ -0,0 +1,5 @@
+
+
+
+ Gbà á, kí o sì ṣe àtuṣe àbá
+
diff --git a/components/compose/awesomebar/src/test/resources/robolectric.properties b/components/compose/awesomebar/src/test/resources/robolectric.properties
deleted file mode 100644
index 89a6c8b4c2e..00000000000
--- a/components/compose/awesomebar/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1 +0,0 @@
-sdk=28
\ No newline at end of file
diff --git a/components/compose/browser-toolbar/src/main/res/values-ast/strings.xml b/components/compose/browser-toolbar/src/main/res/values-ast/strings.xml
new file mode 100644
index 00000000000..8969801e9c2
--- /dev/null
+++ b/components/compose/browser-toolbar/src/main/res/values-ast/strings.xml
@@ -0,0 +1,5 @@
+
+
+
+ Borrar
+
diff --git a/components/compose/browser-toolbar/src/main/res/values-ban/strings.xml b/components/compose/browser-toolbar/src/main/res/values-ban/strings.xml
new file mode 100644
index 00000000000..d5f16e8163a
--- /dev/null
+++ b/components/compose/browser-toolbar/src/main/res/values-ban/strings.xml
@@ -0,0 +1,5 @@
+
+
+
+ Puyung
+
diff --git a/components/compose/browser-toolbar/src/main/res/values-skr/strings.xml b/components/compose/browser-toolbar/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..638b35e354b
--- /dev/null
+++ b/components/compose/browser-toolbar/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+
+
+
+ صاف کرو
+
diff --git a/components/compose/browser-toolbar/src/main/res/values-te/strings.xml b/components/compose/browser-toolbar/src/main/res/values-te/strings.xml
new file mode 100644
index 00000000000..d7291694e8d
--- /dev/null
+++ b/components/compose/browser-toolbar/src/main/res/values-te/strings.xml
@@ -0,0 +1,5 @@
+
+
+
+ తుడిచివేయి
+
diff --git a/components/compose/browser-toolbar/src/main/res/values-yo/strings.xml b/components/compose/browser-toolbar/src/main/res/values-yo/strings.xml
new file mode 100644
index 00000000000..e49144b27cf
--- /dev/null
+++ b/components/compose/browser-toolbar/src/main/res/values-yo/strings.xml
@@ -0,0 +1,5 @@
+
+
+
+ Paárẹ́
+
diff --git a/components/compose/browser-toolbar/src/test/resources/robolectric.properties b/components/compose/browser-toolbar/src/test/resources/robolectric.properties
deleted file mode 100644
index 89a6c8b4c2e..00000000000
--- a/components/compose/browser-toolbar/src/test/resources/robolectric.properties
+++ /dev/null
@@ -1 +0,0 @@
-sdk=28
\ No newline at end of file
diff --git a/components/compose/tabstray/src/main/res/values-skr/strings.xml b/components/compose/tabstray/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..259bf6e8432
--- /dev/null
+++ b/components/compose/tabstray/src/main/res/values-skr/strings.xml
@@ -0,0 +1,7 @@
+
+
+
+ 1 ٹیب کھولو۔ ٹیبز بدلن کیتے دباؤ۔
+
+ %1$s ٹیبز کھولو۔ ٹیبز بدلن کیتے دباؤ۔
+
diff --git a/components/compose/tabstray/src/main/res/values-yo/strings.xml b/components/compose/tabstray/src/main/res/values-yo/strings.xml
new file mode 100644
index 00000000000..702c4c49ecc
--- /dev/null
+++ b/components/compose/tabstray/src/main/res/values-yo/strings.xml
@@ -0,0 +1,7 @@
+
+
+
+ 1 sí táàbù. Fọwọ́ kàn án láti tan táàbù.
+
+ %1$s sí táàbù. Fọwọ́ kàn án láti tan táàbù.
+
diff --git a/components/concept/base/src/main/java/mozilla/components/concept/base/profiler/Profiler.kt b/components/concept/base/src/main/java/mozilla/components/concept/base/profiler/Profiler.kt
index 583b892588c..93a9f2c6472 100644
--- a/components/concept/base/src/main/java/mozilla/components/concept/base/profiler/Profiler.kt
+++ b/components/concept/base/src/main/java/mozilla/components/concept/base/profiler/Profiler.kt
@@ -132,4 +132,24 @@ interface Profiler {
* @param markerName Name of the event as a string.
*/
fun addMarker(markerName: String)
+
+ /**
+ * Start the Gecko profiler with the given settings. This is used by embedders which want to
+ * control the profiler from the embedding app. This allows them to provide an easier access point
+ * to profiling, as an alternative to the traditional way of using a desktop Firefox instance
+ * connected via USB + adb.
+ *
+ * @param aFilters The list of threads to profile, as an array of string of thread names filters.
+ * Each filter is used as a case-insensitive substring match against the actual thread names.
+ * @param aFeaturesArr The list of profiler features to enable for profiling, as a string array.
+ */
+ fun startProfiler(filters: Array, features: Array)
+
+ /**
+ * Stop the profiler and capture the recorded profile. This method is asynchronous.
+ *
+ * @return GeckoResult for the captured profile. The profile is returned as a byte[] buffer
+ * containing a gzip-compressed payload (with gzip header) of the profile JSON.
+ */
+ fun stopProfiler(onSuccess: (ByteArray?) -> Unit, onError: (Throwable) -> Unit)
}
diff --git a/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSession.kt b/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSession.kt
index 77ada0e4ea8..3ee410bfc3f 100644
--- a/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSession.kt
+++ b/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSession.kt
@@ -87,6 +87,11 @@ abstract class EngineSession(
*/
fun onPromptDismissed(promptRequest: PromptRequest) = Unit
+ /**
+ * The engine has requested a prompt update.
+ */
+ fun onPromptUpdate(previousPromptRequestUid: String, promptRequest: PromptRequest) = Unit
+
/**
* User cancelled a repost prompt. Page will not be reloaded.
*/
@@ -569,6 +574,23 @@ abstract class EngineSession(
override fun hashCode() = value
}
+ /**
+ * Represents a session priority, which signals to the engine that it should give
+ * a different prioritization to a given session.
+ */
+ @Suppress("MagicNumber")
+ enum class SessionPriority(val id: Int) {
+ /**
+ * Signals to the engine that this session has a default priority.
+ */
+ DEFAULT(0),
+ /**
+ * Signals to the engine that this session is important, and the Engine should keep
+ * the session alive for as long as possible.
+ */
+ HIGH(1)
+ }
+
/**
* Loads the given URL.
*
@@ -695,6 +717,13 @@ abstract class EngineSession(
*/
open fun markActiveForWebExtensions(active: Boolean) = Unit
+ /**
+ * Updates the priority for this session.
+ *
+ * @param priority the new priority for this session.
+ */
+ open fun updateSessionPriority(priority: SessionPriority) = Unit
+
/**
* Purges the history for the session (back and forward history).
*/
diff --git a/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/CreditCard.kt b/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/CreditCard.kt
deleted file mode 100644
index 1b3b650e219..00000000000
--- a/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/CreditCard.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/* 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.concept.engine.prompt
-
-import android.annotation.SuppressLint
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-/**
- * Value type that represents a credit card.
- *
- * @property guid The unique identifier for this credit card.
- * @property name The credit card billing name.
- * @property number The credit card number.
- * @property expiryMonth The credit card expiry month.
- * @property expiryYear The credit card expiry year.
- * @property cardType The credit card network ID.
- */
-@SuppressLint("ParcelCreator")
-@Parcelize
-data class CreditCard(
- val guid: String?,
- val name: String,
- val number: String,
- val expiryMonth: String,
- val expiryYear: String,
- val cardType: String
-) : Parcelable {
- val obfuscatedCardNumber: String
- get() = ellipsesStart +
- ellipsis + ellipsis + ellipsis + ellipsis +
- number.substring(number.length - digitsToShow) +
- ellipsesEnd
-
- companion object {
- // Left-To-Right Embedding (LTE) mark
- const val ellipsesStart = "\u202A"
-
- // One dot ellipsis
- const val ellipsis = "\u2022\u2060\u2006\u2060"
-
- // Pop Directional Formatting (PDF) mark
- const val ellipsesEnd = "\u202C"
-
- // Number of digits to be displayed after ellipses on an obfuscated credit card number.
- const val digitsToShow = 4
- }
-}
diff --git a/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt b/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt
index 9e9635c1574..f21b49f5a0a 100644
--- a/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt
+++ b/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt
@@ -9,6 +9,8 @@ import android.net.Uri
import mozilla.components.concept.engine.prompt.PromptRequest.Authentication.Level
import mozilla.components.concept.engine.prompt.PromptRequest.Authentication.Method
import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection.Type
+import mozilla.components.concept.storage.Address
+import mozilla.components.concept.storage.CreditCardEntry
import mozilla.components.concept.storage.Login
import mozilla.components.concept.storage.LoginEntry
import java.util.UUID
@@ -91,15 +93,27 @@ sealed class PromptRequest(
val onStay: () -> Unit
) : PromptRequest()
+ /**
+ * Value type that represents a request for a save credit card prompt.
+ * @property creditCard the [CreditCardEntry] to save or update.
+ * @property onConfirm callback that is called when the user confirms the save credit card request.
+ * @property onDismiss callback to let the page know the user dismissed the dialog.
+ */
+ data class SaveCreditCard(
+ val creditCard: CreditCardEntry,
+ val onConfirm: (CreditCardEntry) -> Unit,
+ override val onDismiss: () -> Unit
+ ) : PromptRequest(shouldDismissOnLoad = false), Dismissible
+
/**
* Value type that represents a request for a select credit card prompt.
- * @property creditCards a list of [CreditCard]s to select from.
+ * @property creditCards a list of [CreditCardEntry]s to select from.
* @property onConfirm callback that is called when the user confirms the credit card selection.
* @property onDismiss callback to let the page know the user dismissed the dialog.
*/
data class SelectCreditCard(
- val creditCards: List,
- val onConfirm: (CreditCard) -> Unit,
+ val creditCards: List,
+ val onConfirm: (CreditCardEntry) -> Unit,
override val onDismiss: () -> Unit
) : PromptRequest(), Dismissible
@@ -129,6 +143,21 @@ sealed class PromptRequest(
override val onDismiss: () -> Unit
) : PromptRequest(), Dismissible
+ /**
+ * Value type that represents a request for a select address prompt.
+ *
+ * This prompt is triggered by the user focusing on an address field.
+ *
+ * @property addresses List of addresses for the user to choose from.
+ * @property onConfirm Callback used to confirm the selected address.
+ * @property onDismiss Callback used to dismiss the address prompt.
+ */
+ data class SelectAddress(
+ val addresses: List,
+ val onConfirm: (Address) -> Unit,
+ override val onDismiss: () -> Unit
+ ) : PromptRequest(), Dismissible
+
/**
* Value type that represents a request for an alert prompt to enter a message.
* @property title title of the dialog.
diff --git a/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/CreditCardTest.kt b/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/CreditCardTest.kt
deleted file mode 100644
index 2d2ec2ac4fb..00000000000
--- a/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/CreditCardTest.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/* 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.concept.engine.prompt
-
-import org.junit.Assert.assertEquals
-import org.junit.Test
-
-class CreditCardTest {
-
- @Test
- fun `Create a CreditCard`() {
- val guid = "1"
- val name = "Banana Apple"
- val number = "4111111111111110"
- val last4Digits = "1110"
- val expiryMonth = "5"
- val expiryYear = "2030"
- val cardType = "amex"
- val creditCard = CreditCard(
- guid = guid,
- name = name,
- number = number,
- expiryMonth = expiryMonth,
- expiryYear = expiryYear,
- cardType = cardType
- )
-
- assertEquals(guid, creditCard.guid)
- assertEquals(name, creditCard.name)
- assertEquals(number, creditCard.number)
- assertEquals(expiryMonth, creditCard.expiryMonth)
- assertEquals(expiryYear, creditCard.expiryYear)
- assertEquals(cardType, creditCard.cardType)
- assertEquals(
- CreditCard.ellipsesStart +
- CreditCard.ellipsis + CreditCard.ellipsis + CreditCard.ellipsis + CreditCard.ellipsis +
- last4Digits +
- CreditCard.ellipsesEnd,
- creditCard.obfuscatedCardNumber
- )
- }
-}
diff --git a/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt b/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt
index 72f700878bf..9b43cab5aa1 100644
--- a/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt
+++ b/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt
@@ -20,6 +20,8 @@ import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
import mozilla.components.concept.engine.prompt.PromptRequest.TextPrompt
import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection
import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection.Type
+import mozilla.components.concept.storage.Address
+import mozilla.components.concept.storage.CreditCardEntry
import mozilla.components.concept.storage.Login
import mozilla.components.concept.storage.LoginEntry
import mozilla.components.support.test.mock
@@ -271,7 +273,7 @@ class PromptRequestTest {
@Test
fun `GIVEN a list of credit cards WHEN SelectCreditCard is confirmed or dismissed THEN their respective callback is invoked`() {
- val creditCard = CreditCard(
+ val creditCard = CreditCardEntry(
guid = "id",
name = "Banana Apple",
number = "4111111111111110",
@@ -281,7 +283,7 @@ class PromptRequestTest {
)
var onDismissCalled = false
var onConfirmCalled = false
- var confirmedCreditCard: CreditCard? = null
+ var confirmedCreditCard: CreditCardEntry? = null
val selectCreditCardRequest = SelectCreditCard(
creditCards = listOf(creditCard),
@@ -311,4 +313,52 @@ class PromptRequestTest {
assertFalse(onConfirmCalled)
assertNull(confirmedCreditCard)
}
+
+ @Test
+ fun `WHEN calling confirm or dismiss on the SelectAddress prompt request THEN the respective callback is invoked`() {
+ val address = Address(
+ guid = "1",
+ givenName = "Firefox",
+ additionalName = "-",
+ familyName = "-",
+ organization = "-",
+ streetAddress = "street",
+ addressLevel3 = "address3",
+ addressLevel2 = "address2",
+ addressLevel1 = "address1",
+ postalCode = "1",
+ country = "Country",
+ tel = "1",
+ email = "@"
+ )
+ var onDismissCalled = false
+ var onConfirmCalled = false
+ var confirmedAddress: Address? = null
+
+ val selectAddresPromptRequest = PromptRequest.SelectAddress(
+ addresses = listOf(address),
+ onDismiss = {
+ onDismissCalled = true
+ },
+ onConfirm = {
+ confirmedAddress = it
+ onConfirmCalled = true
+ }
+ )
+
+ assertEquals(selectAddresPromptRequest.addresses, listOf(address))
+
+ selectAddresPromptRequest.onConfirm(address)
+
+ assertTrue(onConfirmCalled)
+ assertFalse(onDismissCalled)
+ assertEquals(address, confirmedAddress)
+
+ onConfirmCalled = false
+
+ selectAddresPromptRequest.onDismiss()
+
+ assertTrue(onDismissCalled)
+ assertFalse(onConfirmCalled)
+ }
}
diff --git a/components/concept/fetch/build.gradle b/components/concept/fetch/build.gradle
index 18b2f919682..1bf9a560c01 100644
--- a/components/concept/fetch/build.gradle
+++ b/components/concept/fetch/build.gradle
@@ -31,6 +31,7 @@ dependencies {
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
testImplementation Dependencies.testing_mockwebserver
+ testImplementation Dependencies.testing_coroutines
testImplementation project(':support-test')
}
diff --git a/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/ClientTest.kt b/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/ClientTest.kt
index d1d1304caf5..5c8c043b229 100644
--- a/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/ClientTest.kt
+++ b/components/concept/fetch/src/test/java/mozilla/components/concept/fetch/ClientTest.kt
@@ -4,14 +4,16 @@
package mozilla.components.concept.fetch
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
class ClientTest {
+ @ExperimentalCoroutinesApi
@Test
- fun `Async request with coroutines`() = runBlocking {
+ fun `Async request with coroutines`() = runTest {
val client = TestClient(responseBody = Response.Body("Hello World".byteInputStream()))
val request = Request("https://www.mozilla.org")
diff --git a/components/concept/storage/build.gradle b/components/concept/storage/build.gradle
index ecc355a17e4..ec60671b4d9 100644
--- a/components/concept/storage/build.gradle
+++ b/components/concept/storage/build.gradle
@@ -27,6 +27,12 @@ dependencies {
// dependency, but it will crash at runtime.
// Included via 'api' because this module is unusable without coroutines.
api Dependencies.kotlin_coroutines
+
+ implementation project(':support-ktx')
+
+ testImplementation project(':support-test')
+ testImplementation Dependencies.testing_junit
+ testImplementation Dependencies.testing_mockito
}
apply from: '../../../publish.gradle'
diff --git a/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt b/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt
index 7ea2412eb51..c4bc0abe20a 100644
--- a/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt
+++ b/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt
@@ -6,8 +6,14 @@ package mozilla.components.concept.storage
import android.annotation.SuppressLint
import android.os.Parcelable
-import kotlinx.coroutines.Deferred
import kotlinx.parcelize.Parcelize
+import mozilla.components.concept.storage.CreditCard.Companion.ellipsesEnd
+import mozilla.components.concept.storage.CreditCard.Companion.ellipsesStart
+import mozilla.components.concept.storage.CreditCard.Companion.ellipsis
+import mozilla.components.support.ktx.kotlin.last4Digits
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
/**
* An interface which defines read/write methods for credit card and address data.
@@ -49,6 +55,7 @@ interface CreditCardsAddressesStorage {
/**
* Deletes the credit card with the given [guid].
*
+ * @param guid Unique identifier for the desired credit card.
* @return True if the deletion did anything, false otherwise.
*/
suspend fun deleteCreditCard(guid: String): Boolean
@@ -196,10 +203,10 @@ data class CreditCard(
val expiryMonth: Long,
val expiryYear: Long,
val cardType: String,
- val timeCreated: Long,
- val timeLastUsed: Long?,
- val timeLastModified: Long,
- val timesUsed: Long
+ val timeCreated: Long = 0L,
+ val timeLastUsed: Long? = 0L,
+ val timeLastModified: Long = 0L,
+ val timesUsed: Long = 0L
) : Parcelable {
val obfuscatedCardNumber: String
get() = ellipsesStart +
@@ -219,6 +226,60 @@ data class CreditCard(
}
}
+/**
+ * Credit card autofill entry.
+ *
+ * This contains the data needed to handle autofill but not the data related to the DB record.
+ *
+ * @property guid The unique identifier for this credit card.
+ * @property name The credit card billing name.
+ * @property number The credit card number.
+ * @property expiryMonth The credit card expiry month.
+ * @property expiryYear The credit card expiry year.
+ * @property cardType The credit card network ID.
+ */
+@Parcelize
+data class CreditCardEntry(
+ val guid: String? = null,
+ val name: String,
+ val number: String,
+ val expiryMonth: String,
+ val expiryYear: String,
+ val cardType: String
+) : Parcelable {
+ val obfuscatedCardNumber: String
+ get() = ellipsesStart +
+ ellipsis + ellipsis + ellipsis + ellipsis +
+ number.last4Digits() +
+ ellipsesEnd
+
+ /**
+ * Credit card expiry date formatted according to the locale. Returns an empty string if either
+ * the expiration month or expiration year is not set.
+ */
+ val expiryDate: String
+ get() {
+ return if (expiryMonth.isEmpty() || expiryYear.isEmpty()) {
+ ""
+ } else {
+ val dateFormat = SimpleDateFormat(DATE_PATTERN, Locale.getDefault())
+
+ val calendar = Calendar.getInstance()
+ calendar.set(Calendar.DAY_OF_MONTH, 1)
+ // Subtract 1 from the expiry month since Calendar.Month is based on a 0-indexed.
+ calendar.set(Calendar.MONTH, expiryMonth.toInt() - 1)
+ calendar.set(Calendar.YEAR, expiryYear.toInt())
+
+ dateFormat.format(calendar.time)
+ }
+ }
+
+ companion object {
+ // Date format pattern for the credit card expiry date.
+ private const val DATE_PATTERN = "MM/yyyy"
+ }
+}
+
/**
* Information about a new credit card.
* Use this when creating a credit card via [CreditCardsAddressesStorage.addCreditCard].
@@ -297,10 +358,10 @@ data class Address(
val country: String,
val tel: String,
val email: String,
- val timeCreated: Long,
- val timeLastUsed: Long?,
- val timeLastModified: Long,
- val timesUsed: Long
+ val timeCreated: Long = 0L,
+ val timeLastUsed: Long? = 0L,
+ val timeLastModified: Long = 0L,
+ val timesUsed: Long = 0L
) : Parcelable
/**
@@ -334,40 +395,85 @@ data class UpdatableAddressFields(
val email: String
)
+/**
+ * Provides a method for checking whether or not a given credit card can be stored.
+ */
+interface CreditCardValidationDelegate {
+
+ /**
+ * The result from validating a given [CreditCard] against the credit card storage. This will
+ * include whether or not it can be created or updated.
+ */
+ sealed class Result {
+ /**
+ * Indicates that the [CreditCard] does not currently exist in the storage, and a new
+ * credit card entry can be created.
+ */
+ object CanBeCreated : Result()
+
+ /**
+ * Indicates that a matching [CreditCard] was found in the storage, and the [CreditCard]
+ * can be used to update its information.
+ */
+ data class CanBeUpdated(val foundCreditCard: CreditCard) : Result()
+ }
+
+ /**
+ * Determines whether a [CreditCardEntry] can be added or updated in the credit card storage.
+ *
+ * @param creditCard [CreditCardEntry] to be added or updated in the credit card storage.
+ * @return [Result] that indicates whether or not the [CreditCardEntry] should be saved or
+ * updated.
+ */
+ suspend fun shouldCreateOrUpdate(creditCard: CreditCardEntry): Result
+}
+
/**
* Used to handle [Address] and [CreditCard] storage so that the underlying engine doesn't have to.
* An instance of this should be attached to the Gecko runtime in order to be used.
*/
-interface CreditCardsAddressesStorageDelegate {
+interface CreditCardsAddressesStorageDelegate : KeyProvider {
/**
* Decrypt a [CreditCardNumber.Encrypted] into its plaintext equivalent or `null` if
* it fails to decrypt.
*
+ * @param key The encryption key to decrypt the decrypt credit card number.
* @param encryptedCardNumber An encrypted credit card number to be decrypted.
* @return A plaintext, non-encrypted credit card number.
*/
- suspend fun decrypt(encryptedCardNumber: CreditCardNumber.Encrypted): CreditCardNumber.Plaintext?
+ suspend fun decrypt(
+ key: ManagedKey,
+ encryptedCardNumber: CreditCardNumber.Encrypted
+ ): CreditCardNumber.Plaintext?
/**
* Returns all stored addresses. This is called when the engine believes an address field
* should be autofilled.
+ *
+ * @return A list of all stored addresses.
*/
- fun onAddressesFetch(): Deferred>
+ suspend fun onAddressesFetch(): List
/**
* Saves the given address to storage.
+ *
+ * @param address [Address] to be saved or updated in the address storage.
*/
- fun onAddressSave(address: Address)
+ suspend fun onAddressSave(address: Address)
/**
* Returns all stored credit cards. This is called when the engine believes a credit card
* field should be autofilled.
+ *
+ * @return A list of all stored credit cards.
*/
- fun onCreditCardsFetch(): Deferred>
+ suspend fun onCreditCardsFetch(): List
/**
* Saves the given credit card to storage.
+ *
+ * @param creditCard [CreditCardEntry] to be saved or updated in the credit card storage.
*/
- fun onCreditCardSave(creditCard: CreditCard)
+ suspend fun onCreditCardSave(creditCard: CreditCardEntry)
}
diff --git a/components/concept/storage/src/test/java/mozilla/components/concept/storage/CreditCardEntryTest.kt b/components/concept/storage/src/test/java/mozilla/components/concept/storage/CreditCardEntryTest.kt
new file mode 100644
index 00000000000..74f803f5ce2
--- /dev/null
+++ b/components/concept/storage/src/test/java/mozilla/components/concept/storage/CreditCardEntryTest.kt
@@ -0,0 +1,72 @@
+/* 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.concept.storage
+
+import mozilla.components.concept.storage.CreditCard.Companion.ellipsesEnd
+import mozilla.components.concept.storage.CreditCard.Companion.ellipsesStart
+import mozilla.components.concept.storage.CreditCard.Companion.ellipsis
+import mozilla.components.support.ktx.kotlin.last4Digits
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class CreditCardEntryTest {
+
+ private val creditCard = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "amex"
+ )
+
+ @Test
+ fun `WHEN obfuscatedCardNumber getter is called THEN the expected obfuscated card number is returned`() {
+ assertEquals(
+ ellipsesStart +
+ ellipsis + ellipsis + ellipsis + ellipsis +
+ creditCard.number.last4Digits() +
+ ellipsesEnd,
+ creditCard.obfuscatedCardNumber
+ )
+ }
+
+ @Test
+ fun `WHEN expiryDdate getter is called THEN the expected expiry date string is returned`() {
+ assertEquals("0${creditCard.expiryMonth}/${creditCard.expiryYear}", creditCard.expiryDate)
+ }
+
+ @Test
+ fun `GIVEN empty expiration date strings WHEN a credit card needs to display its full expiration date THEN the an empty string is returned`() {
+ val creditCardWithoutYear = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "",
+ cardType = "amex"
+ )
+ val creditCardWithoutMonth = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "",
+ expiryYear = "2030",
+ cardType = "amex"
+ )
+ val creditCardWithoutFullDate = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "",
+ expiryYear = "",
+ cardType = "amex"
+ )
+
+ assertEquals("", creditCardWithoutYear.expiryDate)
+ assertEquals("", creditCardWithoutMonth.expiryDate)
+ assertEquals("", creditCardWithoutFullDate.expiryDate)
+ }
+}
diff --git a/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt b/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt
index 733f48ee599..bfd405496cb 100644
--- a/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt
+++ b/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt
@@ -153,15 +153,20 @@ interface Toolbar {
fun addNavigationAction(action: Action)
/**
- * Adds an action to be displayed in edit mode.
+ * Adds an action to be displayed at the start of the URL in edit mode.
*/
fun addEditActionStart(action: Action)
/**
- * Adds an action to be displayed in edit mode.
+ * Adds an action to be displayed at the end of the URL in edit mode.
*/
fun addEditActionEnd(action: Action)
+ /**
+ * Removes an action at the end of the URL in edit mode.
+ */
+ fun removeEditActionEnd(action: Action)
+
/**
* Casts this toolbar to an Android View object.
*/
diff --git a/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/AutoPushObserverTest.kt b/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/AutoPushObserverTest.kt
index 58576df74ad..3cda17c2311 100644
--- a/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/AutoPushObserverTest.kt
+++ b/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/AutoPushObserverTest.kt
@@ -4,11 +4,7 @@
package mozilla.components.feature.accounts.push
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.setMain
import mozilla.components.concept.sync.ConstellationState
import mozilla.components.concept.sync.Device
import mozilla.components.concept.sync.DeviceConstellation
@@ -19,6 +15,9 @@ import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import mozilla.components.support.test.nullable
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.`when`
import org.mockito.Mockito.never
@@ -26,7 +25,11 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoInteractions
import org.mockito.stubbing.OngoingStubbing
+@ExperimentalCoroutinesApi // for runTestOnMain
class AutoPushObserverTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
private val manager: FxaAccountManager = mock()
private val account: OAuthAccount = mock()
private val constellation: DeviceConstellation = mock()
@@ -34,8 +37,7 @@ class AutoPushObserverTest {
@ExperimentalCoroutinesApi
@Test
- fun `messages are forwarded to account manager`() = runBlocking {
- Dispatchers.setMain(TestCoroutineDispatcher())
+ fun `messages are forwarded to account manager`() = runTestOnMain {
val observer = AutoPushObserver(manager, mock(), "test")
`when`(manager.authenticatedAccount()).thenReturn(account)
@@ -48,7 +50,7 @@ class AutoPushObserverTest {
}
@Test
- fun `account manager is not invoked if no account is available`() = runBlocking {
+ fun `account manager is not invoked if no account is available`() = runTestOnMain {
val observer = AutoPushObserver(manager, mock(), "test")
observer.onMessageReceived("test", "foobar".toByteArray())
@@ -59,7 +61,7 @@ class AutoPushObserverTest {
}
@Test
- fun `messages are not forwarded to account manager if they are for a different scope`() = runBlocking {
+ fun `messages are not forwarded to account manager if they are for a different scope`() = runTestOnMain {
val observer = AutoPushObserver(manager, mock(), "fake")
observer.onMessageReceived("test", "foobar".toByteArray())
@@ -70,8 +72,7 @@ class AutoPushObserverTest {
@ExperimentalCoroutinesApi
@Test
- fun `subscription changes are forwarded to account manager`() = runBlocking {
- Dispatchers.setMain(TestCoroutineDispatcher())
+ fun `subscription changes are forwarded to account manager`() = runTestOnMain {
val observer = AutoPushObserver(manager, pushFeature, "test")
whenSubscribe()
@@ -91,7 +92,7 @@ class AutoPushObserverTest {
}
@Test
- fun `do nothing if there is no account manager`() = runBlocking {
+ fun `do nothing if there is no account manager`() = runTestOnMain {
val observer = AutoPushObserver(manager, pushFeature, "test")
whenSubscribe()
@@ -103,7 +104,7 @@ class AutoPushObserverTest {
}
@Test
- fun `subscription changes are not forwarded to account manager if they are for a different scope`() = runBlocking {
+ fun `subscription changes are not forwarded to account manager if they are for a different scope`() = runTestOnMain {
val observer = AutoPushObserver(manager, mock(), "fake")
`when`(manager.authenticatedAccount()).thenReturn(account)
diff --git a/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/ConstellationObserverTest.kt b/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/ConstellationObserverTest.kt
index 76b218e8de8..a45cba6417c 100644
--- a/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/ConstellationObserverTest.kt
+++ b/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/ConstellationObserverTest.kt
@@ -8,7 +8,7 @@ package mozilla.components.feature.accounts.push
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.concept.base.crash.CrashReporting
import mozilla.components.concept.sync.ConstellationState
import mozilla.components.concept.sync.Device
@@ -22,6 +22,7 @@ import mozilla.components.support.test.eq
import mozilla.components.support.test.mock
import mozilla.components.support.test.nullable
import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -32,6 +33,7 @@ import org.mockito.Mockito.verifyNoInteractions
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.stubbing.OngoingStubbing
+@ExperimentalCoroutinesApi // for runTestOnMain
@RunWith(AndroidJUnit4::class)
class ConstellationObserverTest {
@@ -56,7 +58,7 @@ class ConstellationObserverTest {
val coroutinesTestRule = MainCoroutineRule()
@Test
- fun `first subscribe works`() = runBlocking {
+ fun `first subscribe works`() = runTestOnMain {
val observer = ConstellationObserver(context, push, "testScope", account, verifier, crashReporter)
verifyNoInteractions(push)
@@ -74,7 +76,7 @@ class ConstellationObserverTest {
}
@Test
- fun `re-subscribe doesn't update constellation on same endpoint`() = runBlocking {
+ fun `re-subscribe doesn't update constellation on same endpoint`() = runTestOnMain {
val observer = ConstellationObserver(context, push, "testScope", account, verifier, crashReporter)
verifyNoInteractions(push)
@@ -93,7 +95,7 @@ class ConstellationObserverTest {
}
@Test
- fun `re-subscribe update constellations on same endpoint if expired`() = runBlocking {
+ fun `re-subscribe update constellations on same endpoint if expired`() = runTestOnMain {
val observer = ConstellationObserver(context, push, "testScope", account, verifier, crashReporter)
verifyNoInteractions(push)
diff --git a/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabFeatureKtTest.kt b/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabFeatureKtTest.kt
index e991c17035e..4b7fa7c135e 100644
--- a/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabFeatureKtTest.kt
+++ b/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabFeatureKtTest.kt
@@ -5,7 +5,7 @@
package mozilla.components.feature.accounts.push
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.sync.DeviceConstellation
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.service.fxa.manager.FxaAccountManager
@@ -21,7 +21,7 @@ import org.mockito.Mockito.verify
@ExperimentalCoroutinesApi
class SendTabFeatureKtTest {
@Test
- fun `feature register all observers`() = runBlockingTest {
+ fun `feature register all observers`() = runTest {
val accountManager: FxaAccountManager = mock()
SendTabFeature(
diff --git a/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabUseCasesTest.kt b/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabUseCasesTest.kt
index df075193329..6c2ac871727 100644
--- a/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabUseCasesTest.kt
+++ b/components/feature/accounts-push/src/test/java/mozilla/components/feature/accounts/push/SendTabUseCasesTest.kt
@@ -5,8 +5,6 @@
package mozilla.components.feature.accounts.push
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.concept.sync.ConstellationState
import mozilla.components.concept.sync.Device
import mozilla.components.concept.sync.DeviceCapability
@@ -18,8 +16,11 @@ import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.support.test.any
import mozilla.components.support.test.eq
import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.`when`
import org.mockito.Mockito.never
@@ -30,6 +31,9 @@ import java.util.UUID
@ExperimentalCoroutinesApi
class SendTabUseCasesTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
private val manager: FxaAccountManager = mock()
private val account: OAuthAccount = mock()
private val constellation: DeviceConstellation = mock()
@@ -43,7 +47,7 @@ class SendTabUseCasesTest {
}
@Test
- fun `SendTabUseCase - tab is sent to capable device`() = runBlockingTest {
+ fun `SendTabUseCase - tab is sent to capable device`() = runTestOnMain {
val useCases = SendTabUseCases(manager, coroutineContext)
val device: Device = generateDevice()
@@ -57,7 +61,7 @@ class SendTabUseCasesTest {
}
@Test
- fun `SendTabUseCase - tabs are sent to capable device`() = runBlockingTest {
+ fun `SendTabUseCase - tabs are sent to capable device`() = runTestOnMain {
val useCases = SendTabUseCases(manager, coroutineContext)
val device: Device = generateDevice()
val tab = TabData("Title", "http://example.com")
@@ -72,7 +76,7 @@ class SendTabUseCasesTest {
}
@Test
- fun `SendTabUseCase - tabs are NOT sent to incapable devices`() = runBlockingTest {
+ fun `SendTabUseCase - tabs are NOT sent to incapable devices`() = runTestOnMain {
val useCases = SendTabUseCases(manager, coroutineContext)
val device: Device = mock()
val tab = TabData("Title", "http://example.com")
@@ -92,7 +96,7 @@ class SendTabUseCasesTest {
}
@Test
- fun `SendTabUseCase - ONLY tabs with valid schema are sent to capable device`() = runBlockingTest {
+ fun `SendTabUseCase - ONLY tabs with valid schema are sent to capable device`() = runTestOnMain {
val useCases = SendTabUseCases(manager, coroutineContext)
val device: Device = generateDevice()
val tab = TabData("Title", "http://example.com")
@@ -109,7 +113,7 @@ class SendTabUseCasesTest {
}
@Test
- fun `SendTabUseCase - device id does not match when sending single tab`() = runBlockingTest {
+ fun `SendTabUseCase - device id does not match when sending single tab`() = runTestOnMain {
val useCases = SendTabUseCases(manager, coroutineContext)
val device: Device = generateDevice("123")
val tab = TabData("Title", "http://example.com")
@@ -132,7 +136,7 @@ class SendTabUseCasesTest {
}
@Test
- fun `SendTabUseCase - device id does not match when sending tabs`() = runBlockingTest {
+ fun `SendTabUseCase - device id does not match when sending tabs`() = runTestOnMain {
val useCases = SendTabUseCases(manager, coroutineContext)
val device: Device = generateDevice("123")
val tab = TabData("Title", "http://example.com")
@@ -155,7 +159,7 @@ class SendTabUseCasesTest {
}
@Test
- fun `SendTabToAllUseCase - tab is sent to capable devices`() = runBlockingTest {
+ fun `SendTabToAllUseCase - tab is sent to capable devices`() = runTestOnMain {
val useCases = SendTabUseCases(manager, coroutineContext)
val device: Device = generateDevice()
val device2: Device = generateDevice()
@@ -172,7 +176,7 @@ class SendTabUseCasesTest {
}
@Test
- fun `SendTabToAllUseCase - tabs is sent to capable devices`() = runBlockingTest {
+ fun `SendTabToAllUseCase - tabs is sent to capable devices`() = runTestOnMain {
val useCases = SendTabUseCases(manager, coroutineContext)
val device: Device = generateDevice()
val device2: Device = generateDevice()
@@ -190,53 +194,49 @@ class SendTabUseCasesTest {
}
@Test
- fun `SendTabToAllUseCase - tab is NOT sent to incapable devices`() {
+ fun `SendTabToAllUseCase - tab is NOT sent to incapable devices`() = runTestOnMain {
val useCases = SendTabUseCases(manager)
val tab = TabData("Mozilla", "https://mozilla.org")
val device: Device = mock()
val device2: Device = mock()
- runBlocking {
- useCases.sendToAllAsync(tab)
+ useCases.sendToAllAsync(tab)
- verify(constellation, never()).sendCommandToDevice(any(), any())
+ verify(constellation, never()).sendCommandToDevice(any(), any())
- `when`(device.id).thenReturn("123")
- `when`(device2.id).thenReturn("456")
- `when`(state.otherDevices).thenReturn(listOf(device, device2))
+ `when`(device.id).thenReturn("123")
+ `when`(device2.id).thenReturn("456")
+ `when`(state.otherDevices).thenReturn(listOf(device, device2))
- useCases.sendToAllAsync(tab)
+ useCases.sendToAllAsync(tab)
- verify(constellation, never()).sendCommandToDevice(any(), any())
- }
+ verify(constellation, never()).sendCommandToDevice(any(), any())
}
@Test
- fun `SendTabToAllUseCase - tabs are NOT sent to capable devices`() {
+ fun `SendTabToAllUseCase - tabs are NOT sent to capable devices`() = runTestOnMain {
val useCases = SendTabUseCases(manager)
val tab = TabData("Mozilla", "https://mozilla.org")
val tab2 = TabData("Firefox", "https://firefox.com")
val device: Device = mock()
val device2: Device = mock()
- runBlocking {
- useCases.sendToAllAsync(tab)
+ useCases.sendToAllAsync(tab)
- verify(constellation, never()).sendCommandToDevice(any(), any())
+ verify(constellation, never()).sendCommandToDevice(any(), any())
- `when`(device.id).thenReturn("123")
- `when`(device2.id).thenReturn("456")
- `when`(state.otherDevices).thenReturn(listOf(device, device2))
+ `when`(device.id).thenReturn("123")
+ `when`(device2.id).thenReturn("456")
+ `when`(state.otherDevices).thenReturn(listOf(device, device2))
- useCases.sendToAllAsync(listOf(tab, tab2))
+ useCases.sendToAllAsync(listOf(tab, tab2))
- verify(constellation, never()).sendCommandToDevice(eq("123"), any())
- verify(constellation, never()).sendCommandToDevice(eq("456"), any())
- }
+ verify(constellation, never()).sendCommandToDevice(eq("123"), any())
+ verify(constellation, never()).sendCommandToDevice(eq("456"), any())
}
@Test
- fun `SendTabToAllUseCase - ONLY tabs with valid schema are sent to capable devices`() = runBlockingTest {
+ fun `SendTabToAllUseCase - ONLY tabs with valid schema are sent to capable devices`() = runTestOnMain {
val useCases = SendTabUseCases(manager, coroutineContext)
val device: Device = generateDevice()
val device2: Device = generateDevice()
@@ -258,7 +258,7 @@ class SendTabUseCasesTest {
}
@Test
- fun `SendTabUseCase - result is false if any send tab action fails`() = runBlockingTest {
+ fun `SendTabUseCase - result is false if any send tab action fails`() = runTestOnMain {
val useCases = SendTabUseCases(manager, coroutineContext)
val device: Device = mock()
val tab = TabData("Title", "http://example.com")
@@ -280,43 +280,39 @@ class SendTabUseCasesTest {
}
@Test
- fun `filter devices returns capable devices`() {
+ fun `filter devices returns capable devices`() = runTestOnMain {
var executed = false
- runBlocking {
- `when`(state.otherDevices).thenReturn(listOf(generateDevice(), generateDevice()))
- filterSendTabDevices(manager) { _, _ ->
- executed = true
- }
-
- Assert.assertTrue(executed)
+ `when`(state.otherDevices).thenReturn(listOf(generateDevice(), generateDevice()))
+ filterSendTabDevices(manager) { _, _ ->
+ executed = true
}
+
+ Assert.assertTrue(executed)
}
@Test
- fun `filter devices does NOT provide for incapable devices`() {
+ fun `filter devices does NOT provide for incapable devices`() = runTestOnMain {
val device: Device = mock()
val device2: Device = mock()
- runBlocking {
- `when`(device.id).thenReturn("123")
- `when`(device2.id).thenReturn("456")
- `when`(state.otherDevices).thenReturn(listOf(device, device2))
-
- filterSendTabDevices(manager) { _, filteredDevices ->
- Assert.assertTrue(filteredDevices.isEmpty())
- }
-
- val accountManager: FxaAccountManager = mock()
- val account: OAuthAccount = mock()
- val constellation: DeviceConstellation = mock()
- val state: ConstellationState = mock()
- `when`(accountManager.authenticatedAccount()).thenReturn(account)
- `when`(account.deviceConstellation()).thenReturn(constellation)
- `when`(constellation.state()).thenReturn(state)
-
- filterSendTabDevices(mock()) { _, _ ->
- Assert.fail()
- }
+ `when`(device.id).thenReturn("123")
+ `when`(device2.id).thenReturn("456")
+ `when`(state.otherDevices).thenReturn(listOf(device, device2))
+
+ filterSendTabDevices(manager) { _, filteredDevices ->
+ Assert.assertTrue(filteredDevices.isEmpty())
+ }
+
+ val accountManager: FxaAccountManager = mock()
+ val account: OAuthAccount = mock()
+ val constellation: DeviceConstellation = mock()
+ val state: ConstellationState = mock()
+ `when`(accountManager.authenticatedAccount()).thenReturn(account)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+ `when`(constellation.state()).thenReturn(state)
+
+ filterSendTabDevices(mock()) { _, _ ->
+ Assert.fail()
}
}
diff --git a/components/feature/accounts/build.gradle b/components/feature/accounts/build.gradle
index e128933c515..c37b33099e1 100644
--- a/components/feature/accounts/build.gradle
+++ b/components/feature/accounts/build.gradle
@@ -45,6 +45,7 @@ dependencies {
testImplementation Dependencies.androidx_test_junit
testImplementation Dependencies.testing_mockito
testImplementation Dependencies.testing_robolectric
+ testImplementation Dependencies.testing_coroutines
testImplementation project(':support-test')
}
diff --git a/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeatureTest.kt b/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeatureTest.kt
index 00546b9377a..689aca6c851 100644
--- a/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeatureTest.kt
+++ b/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FirefoxAccountsAuthFeatureTest.kt
@@ -8,7 +8,7 @@ import android.content.Context
import android.os.Looper.getMainLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.engine.request.RequestInterceptor
import mozilla.components.concept.sync.AccountEventsObserver
import mozilla.components.concept.sync.AuthFlowUrl
@@ -76,7 +76,7 @@ class FirefoxAccountsAuthFeatureTest {
@Config(sdk = [22])
@Test
- fun `begin authentication`() = runBlocking {
+ fun `begin authentication`() = runTest {
val manager = prepareAccountManagerForSuccessfulAuthentication(
this.coroutineContext
)
@@ -95,7 +95,7 @@ class FirefoxAccountsAuthFeatureTest {
@Config(sdk = [22])
@Test
- fun `begin pairing authentication`() = runBlocking {
+ fun `begin pairing authentication`() = runTest {
val manager = prepareAccountManagerForSuccessfulAuthentication(
this.coroutineContext
)
@@ -114,7 +114,7 @@ class FirefoxAccountsAuthFeatureTest {
@Config(sdk = [22])
@Test
- fun `begin authentication with errors`() = runBlocking {
+ fun `begin authentication with errors`() = runTest {
val manager = prepareAccountManagerForFailedAuthentication(
this.coroutineContext
)
@@ -135,7 +135,7 @@ class FirefoxAccountsAuthFeatureTest {
@Config(sdk = [22])
@Test
- fun `begin pairing authentication with errors`() = runBlocking {
+ fun `begin pairing authentication with errors`() = runTest {
val manager = prepareAccountManagerForFailedAuthentication(
this.coroutineContext
)
@@ -155,7 +155,7 @@ class FirefoxAccountsAuthFeatureTest {
}
@Test
- fun `auth interceptor`() = runBlocking {
+ fun `auth interceptor`() = runTest {
val manager = mock()
val redirectUrl = "https://accounts.firefox.com/oauth/success/123"
val feature = FirefoxAccountsAuthFeature(
diff --git a/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FxaWebChannelFeatureTest.kt b/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FxaWebChannelFeatureTest.kt
index 40fe978fa12..adaaa47b051 100644
--- a/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FxaWebChannelFeatureTest.kt
+++ b/components/feature/accounts/src/test/java/mozilla/components/feature/accounts/FxaWebChannelFeatureTest.kt
@@ -6,7 +6,7 @@ package mozilla.components.feature.accounts
import android.os.Looper.getMainLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
@@ -579,7 +579,7 @@ class FxaWebChannelFeatureTest {
// Receiving an oauth-login message account manager accepts the request
@Test
- fun `COMMAND_OAUTH_LOGIN web-channel must be processed through when the accountManager accepts the request`() = runBlocking {
+ fun `COMMAND_OAUTH_LOGIN web-channel must be processed through when the accountManager accepts the request`() = runTest {
val accountManager: FxaAccountManager = mock() // syncConfig is null by default (is not configured)
val engineSession: EngineSession = mock()
val ext: WebExtension = mock()
@@ -612,7 +612,7 @@ class FxaWebChannelFeatureTest {
// Receiving an oauth-login message account manager refuses the request
@Test
- fun `COMMAND_OAUTH_LOGIN web-channel must be processed when the accountManager refuses the request`() = runBlocking {
+ fun `COMMAND_OAUTH_LOGIN web-channel must be processed when the accountManager refuses the request`() = runTest {
val accountManager: FxaAccountManager = mock() // syncConfig is null by default (is not configured)
val engineSession: EngineSession = mock()
val ext: WebExtension = mock()
diff --git a/components/feature/addons/src/main/res/values-ast/strings.xml b/components/feature/addons/src/main/res/values-ast/strings.xml
index 0f7075cfac4..fd4c7f7b37e 100644
--- a/components/feature/addons/src/main/res/values-ast/strings.xml
+++ b/components/feature/addons/src/main/res/values-ast/strings.xml
@@ -75,7 +75,7 @@
Páxina d\'aniciu
- Deprender más tocante a los permisos
+ Saber más tocante a los permisosValoración
@@ -97,7 +97,7 @@
Aconséyase
- Entá nun se sofita
+ Entá nun ye compatibleEntá nun ta disponible
@@ -109,7 +109,7 @@
Quitar
- ¿Amestar %1$s?
+ ¿Quies amestar «%1$s»?Rique\'l to permisu pa:
@@ -131,9 +131,9 @@
Permitir
- Ñegar
+ Negar
- %1$s tien un anovamientu
+ «%1$s» tien un anovamientuRíquense %1$d permisos nuevos
@@ -141,17 +141,17 @@
Anovamientos de complementos
- Comprobador de complementos sofitaos
+ Comprobador de complementos compatiblesHai un complementu nuevu disponibleHai complementos nuevos disponibles
- Amestar %1$s a %2$s
+ Amestar «%1$s» a %2$s
- Amestar %1$s y %2$s a %3$s
+ Amestar «%1$s» y «%2$s» a «%3$s»
- Amestalos a %1$s
+ Amiéstalos a %1$sLa teunoloxía de complementos pa Firefox ta modernizándose. Estos complementos usen frameworks que nun son compatibles a partir de Firefox 75.
@@ -161,27 +161,27 @@
¡Hebo un fallu al solicitar los complementos!
- Nun s\'atopó la traducción de la locale %1$s nin de la llingua predeterminada %2$s
+ Nun s\'atopó la traducción de la locale «%1$s» nin de la llingua predeterminada «%2$s»
- %1$s instalóse con ésitu
+ «%1$s» instalóse correutamente
- Hebo un fallu al instalar %1$s
+ Hebo un fallu al instalar «%1$s»
- %1$s activóse con ésitu
+ «%1$s» activóse correutamente
- Hebo un fallu al activar %1$s
+ Hebo un fallu al activar «%1$s»
- %1$s desactivóse con ésitu
+ «%1$s» desactivóse correutamente
- Hebo un fallu al desactivar %1$s
+ Hebo un fallu al desactivar «%1$s»
- %1$s desinstalóse con ésitu
+ «%1$s» desinstalóse correutamente
- Hebo un fallu al desinstalar %1$s
+ Hebo un fallu al desinstalar «%1$s»
- %1$s quitóse con ésitu
+ «%1$s» quitóse correutamante
- Hebo un fallu al quitar %1$s
+ Hebo un fallu al quitar «%1$s»Esti complementu migró dende una versión anterior de %1$s
@@ -189,9 +189,9 @@
%1$s complementos
- Deprender más
+ Saber más
- Anovóse con ésitu
+ Anovóse correutamenteNun hai nengún anovamientu disponible
@@ -203,9 +203,9 @@
Estáu:
- %1$s amestóse a %2$s
+ «%1$s» amestóse a %2$sÁbrilu nel menú
- Val, entiéndolo
+ Entiéndolo
diff --git a/components/feature/addons/src/main/res/values-cs/strings.xml b/components/feature/addons/src/main/res/values-cs/strings.xml
index 460fd810fe6..9286c7b9b11 100644
--- a/components/feature/addons/src/main/res/values-cs/strings.xml
+++ b/components/feature/addons/src/main/res/values-cs/strings.xml
@@ -179,7 +179,7 @@
Doplněk %1$s se nepodařilo odinstalovat
- Doplněk %1$s byl úspěšně odebrát
+ Doplněk %1$s byl úspěšně odebránDoplněk %1$s se nepodařilo odebrat
diff --git a/components/feature/addons/src/main/res/values-ka/strings.xml b/components/feature/addons/src/main/res/values-ka/strings.xml
index e4744106952..7dba515d7b8 100644
--- a/components/feature/addons/src/main/res/values-ka/strings.xml
+++ b/components/feature/addons/src/main/res/values-ka/strings.xml
@@ -23,11 +23,11 @@
%1$d will be replaced by an integer indicating the number of additional domains for which this web extension is requesting permission. -->
თქვენს მონაცემებთან წვდომა, %1$d სხვა მისამართზე
- ბრაუზერის ჩანართებზე წვდომა
+ ბრაუზერის ჩანართებთან წვდომაგანუსაზღვრელი მოცულობის მონაცემების შენახვა დისკზე
- გვერდების მონახულებისას, ბრაუზერის მოქმედებებზე წვდომა
+ გვერდების მონახულებისას, ბრაუზერის მოქმედებებთან წვდომასანიშნების ნახვა და შეცვლა
@@ -45,9 +45,9 @@
ტექსტის წაკითხვა ყველა გახსნილი ჩანართიდან
- მდებარეობის მონაცემებზე წვდომა
+ მდებარეობის მონაცემებთან წვდომა
- მონახულებული გვერდების ისტორიაზე წვდომა
+ მონახულებული გვერდების ისტორიასთან წვდომაგაფართოების მოხმარების შესახებ მონაცემების შეგროვება და თემების მართვა
@@ -63,7 +63,7 @@
ბრაუზერის ჩანართების დამალვა და გამოჩენა
- მონახულებული გვერდების ისტორიაზე წვდომა
+ მონახულებული გვერდების ისტორიასთან წვდომაშემმუშავებლის ხელსაწყოების გამოყენება, გახსნილ ჩანართებზე თქვენს მონაცემებთან წვდომისთვის
diff --git a/components/feature/addons/src/main/res/values-kab/strings.xml b/components/feature/addons/src/main/res/values-kab/strings.xml
index 09640eed0fa..d5d1517a507 100644
--- a/components/feature/addons/src/main/res/values-kab/strings.xml
+++ b/components/feature/addons/src/main/res/values-kab/strings.xml
@@ -39,7 +39,7 @@
Sekcem isefka ɣer tkatut Ɣef afus
- Sider-d ifuyla, ɣeṛ daɣen beddel amazray n usider deg iminig
+ Sader-d ifuyla, ɣeṛ daɣen beddel amazray n usader deg iminigLdi ifuya i d-yudren deg yibenk-ik
diff --git a/components/feature/addons/src/main/res/values-skr/strings.xml b/components/feature/addons/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..71ed5bdbce1
--- /dev/null
+++ b/components/feature/addons/src/main/res/values-skr/strings.xml
@@ -0,0 +1,211 @@
+
+
+
+ رازداری ترتیباں پڑھو تے تبدیل کرو
+
+ ساری ویب سائٹس کیتے آپݨے ڈیٹا تائیں رسائی گھنو
+
+ %1$s کیتے آپݨے ڈیٹا تائیں رسائی گھنو۔
+
+ %1$s ڈومین وچ آپݨی سائٹس دے ڈیٹا تے رسائی حاصل کرو
+
+ 1 ٻئی سائٹ تے آپݨے ڈیٹا تائیں رسائی گھنو۔
+
+ %1$d ٻیاں سائٹاں تے آپݨے ڈیٹا تائیں رسائی گھنو
+
+ 1 ٻئی ڈومین تے آپݨے ڈیٹا تائیں رسائی گھنو۔
+
+ %1$d ٻئی ڈومیناں تے آپݨے ڈیٹا تے رسائی گھنو
+
+ براؤزر ٹیبز تائیں رسائی حاصل کرو
+
+ گاہک دی طرفوں بے انت ڈیٹا ذخیرہ کرو
+
+ نیویگیشݨ دے دوران براوئزر دی سرگرمی تائیں رسائی
+
+ کتاب نشان پڑھو تے ترمیم کرو
+
+ براؤزر ترتیباں پڑھو تے تبدیل کرو
+
+ حالیہ براؤزنگ تاریخ۔ کوکیاں تے متعلقہ ڈیٹا صاف کرو
+
+ کلپ بورڈ کنوں ڈیٹا گھنو
+
+ کلپ بورڈ وچ ڈیٹا پاؤ
+
+ فائلاں ڈاؤن لوڈ کرو تے براؤزر دی ڈاؤن لوڈ تاریخ پڑھو تے تجدید کرو
+
+ آپݨے ڈیوائس تے ڈاؤن لوڈ تھیاں فائلاں کھولو
+
+ سارے کھلے ٹیباں دی عبارت پڑھو
+
+ آپݨے مقام تائیں اپڑو
+
+ براؤزنگ تاریخ تے اپڑو
+
+ ایکسٹنشن ورتاوے دی نگرانی کرو تے تھیم منیج کرو
+
+ ایں ایپ دے علاوہ کہیں ٻئی ایپ نال سنیہاں دا تبادلہ کرو
+
+ تہاݙے کیتے اطلاع نامیاں دی نمائش تھیوے
+
+ کریپٹو گرافک تصدیق دیاں خدمات فراہم کرو
+
+ براؤزر دیاں پراکسی ترتیباں کوں کنٹرول کرو
+
+ حالیہ بند تھیاں ٹیباں تے اپڑو
+
+ براؤزر ٹیباں لکاؤ تے ݙکھاؤ
+
+ براؤزنگ تاریخ تے اپڑو
+
+ کھلے ٹیب وچ آپݨے ڈیٹا تائیں رسائی کیتے ڈویلپر دے اوزار ودھاؤ
+
+ ورشن
+
+ مصنف
+
+ چھیکڑی واری اپ ڈیٹ تھیا
+
+ مکھ پناں
+
+ اجازتاں بارے ٻیا سکھو
+
+ ریٹنگ
+
+ ترتیباں
+
+ چالو
+
+ بند
+
+ نجی براؤزنگ وچ اجازت ݙیوو
+
+ نجی براؤزنگ وچ چلاؤ
+
+ فعال تھیا
+
+ غیرفعال تھیا
+
+ انسٹال تھیا
+
+ سفارش تھئے ہوئے
+
+ اڄݨ تائیں سہارا تھیا کائنی
+
+ اڄݨ تائیں دستاب کائنی
+
+ غیرفعال تھیا
+
+ تفصیلاں
+
+ اجازتاں
+
+ ہٹاؤ
+
+ %1$s شامل کروں؟
+
+ ایں وچ تہاݙی اجازت ضروری ہے:
+
+ شامل کرو
+
+ منسوخ
+
+ ایڈ ــ آن انسٹال کرو
+
+ منسوخ
+
+ جائزہ:%1$s
+
+ %1$.02f/5
+
+ ایڈ ــ آن
+
+ ایڈ ــ آن منیجر
+
+ اجازت ݙیوو
+
+ انکار کرو
+
+ %1$s کیتے ہک نویں اپ ڈیٹ ہے
+
+ %1$d کوں نویاں اجازتاں دی لوڑ ہے
+
+ نویں اجازت ضروری ہے
+
+ ایڈ ــ آن اپ ڈیٹاں
+
+ سہارا تھئے ایڈ ــ آن پڑتال کار
+
+ نویں ایڈ ــ آن دستیاب ہے
+
+ نویں ایڈ ــ آن دستیاب ہے
+
+ %2$s وچ %1$s شامل کرو
+
+ %1$s تے %2$s کوں %3$s وچ جوڑو
+
+ اُنہاں کوں %1$s وچ شامل کرو
+
+ فائرفوکس دی ایڈ ــ آن ٹیکنالوجی جدید بݨدی پئی ہے۔ ایہ ایڈ ــ آن اینجھے فریم ورک ورتیندن جہڑے فائرفوکس ٧٥ تے اوں کنوں اڳوں دے ورشن دے موافق کائنی۔
+
+ ایکسٹنشناساں فی الحال تجویز تھئے ایکسٹنشناں دے موہری انتخاب کیتے سہارا بݨیندے پئے ہیں۔
+
+ ایڈ ــ آن کوں ڈاؤن لوڈ تے تصدیق کریندا پئے۔۔۔
+
+ ایڈ ــ آن دریافت کرݨ وچ ناکام!
+
+ %1$s زبان کیتے تے نہ ہی پہلوں مقرر زبان %2$s کیتے ترجمہ لبھے
+
+ %1$s کامیابی نال انسٹال تھی ڳیا
+
+ %1$s انسٹال تھیوݨ وچ ناکام تھیا
+
+ %1$s کامیابی نال فعال تھی ڳیا
+
+ %1$s فعال کرݨ وچ ناکام تھیا
+
+ %1$s کامیابی نال غیرفعال تھی ڳیا
+
+ %1$s غیرفعال کرݨ وچ ناکام تھیا
+
+ %1$s کامیابی نال اݨ انسٹال تھی ڳیا
+
+ %1$s اݨ انسٹال تھیوݨ وچ ناکام تھیا
+
+ %1$s کامیابی نال ہٹ ڳیا
+
+ %1$s ہٹاوݨ وچ ناکام تھیا
+
+ ایہ ایڈ ــ آن %1$s دے پچھلے ورشن کنوں منتقل کیتا ڳیا ہائی
+
+ 1 ایڈ ــ آن
+
+ %1$s ایڈ ــ آن
+
+ ٻیا سِکھو
+
+ کامیابی نال اپ ڈیٹ تھی ڳیا
+
+ اڄݨ تائیں کوئی اپ ڈیٹ دستاب کائنی
+
+ نقص
+
+ اپڈیٹر ڄاݨکاری
+
+ چھیکڑی کوشش:
+
+ حیثیت:
+
+ %2$s وچ %1$s شامل تھی ڳیا ہے
+
+ ایں کوں مینیو وچ کھولو
+
+ ٹھیک ہے، سمجھ آ ڳیا
+
diff --git a/components/feature/addons/src/main/res/values-ur/strings.xml b/components/feature/addons/src/main/res/values-ur/strings.xml
index 0e23d1fe77b..714a1cc8d3d 100644
--- a/components/feature/addons/src/main/res/values-ur/strings.xml
+++ b/components/feature/addons/src/main/res/values-ur/strings.xml
@@ -29,7 +29,7 @@
گشت کاری کے دوران برائزر کی سرگرمی کی رسائی
- نشانیاں پڑھیں اور ترمیم کریں
+ بک مارک پڑھیں اور ترمیم کریںرازداری سیٹکگیں پڑھیں اور ترمیم کریں
diff --git a/components/feature/addons/src/main/res/values-yo/strings.xml b/components/feature/addons/src/main/res/values-yo/strings.xml
new file mode 100644
index 00000000000..68417aca760
--- /dev/null
+++ b/components/feature/addons/src/main/res/values-yo/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ Kà á, kí o sì ṣe àtuṣe sí ààtò àsírí
+
+ Ní àǹfààní sí data rẹ̀ ní gbogbo ìkànnì
+
+ Ní àǹfààní sí data rẹ ní %1$s
+
+ Ní àǹfààní sí data rẹ fún àwọn ìkànnì ní agbegbe %1$s
+
+ Ní àǹfààní sí data rẹ ni 1 ìkànnì mìíràn
+
+ Ní àǹfààní sí data rẹ lórí %1$d ìkànnì mìíràn
+
+ Ní àǹfààní sí data rẹ lórí 1 agbegbe mìíràn
+
diff --git a/components/feature/addons/src/test/java/AddonManagerTest.kt b/components/feature/addons/src/test/java/AddonManagerTest.kt
index 75cdfaa807d..d93550c826e 100644
--- a/components/feature/addons/src/test/java/AddonManagerTest.kt
+++ b/components/feature/addons/src/test/java/AddonManagerTest.kt
@@ -10,7 +10,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
import mozilla.components.browser.state.action.WebExtensionAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.WebExtensionState
@@ -30,6 +29,7 @@ import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.eq
import mozilla.components.support.test.mock
import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import mozilla.components.support.test.whenever
import mozilla.components.support.webextensions.WebExtensionSupport
import org.junit.After
@@ -68,7 +68,7 @@ class AddonManagerTest {
}
@Test
- fun `getAddons - queries addons from provider and updates installation state`() = runBlocking {
+ fun `getAddons - queries addons from provider and updates installation state`() = runTestOnMain {
// Prepare addons provider
val addon1 = Addon(id = "ext1")
val addon2 = Addon(id = "ext2")
@@ -156,7 +156,7 @@ class AddonManagerTest {
}
@Test
- fun `getAddons - returns temporary add-ons as supported`() = runBlocking {
+ fun `getAddons - returns temporary add-ons as supported`() = runTestOnMain {
val addonsProvider: AddonsProvider = mock()
whenever(addonsProvider.getAvailableAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(listOf())
@@ -199,7 +199,7 @@ class AddonManagerTest {
}
@Test(expected = AddonManagerException::class)
- fun `getAddons - wraps exceptions and rethrows them`() = runBlocking {
+ fun `getAddons - wraps exceptions and rethrows them`() = runTestOnMain {
val store = BrowserStore()
val engine: Engine = mock()
@@ -217,7 +217,7 @@ class AddonManagerTest {
}
@Test
- fun `getAddons - filters unneeded locales`() = runBlocking {
+ fun `getAddons - filters unneeded locales`() = runTestOnMain {
val addon = Addon(
id = "addon1",
translatableName = mapOf(Addon.DEFAULT_LOCALE to "name", "invalid1" to "Name", "invalid2" to "nombre"),
@@ -247,7 +247,7 @@ class AddonManagerTest {
}
@Test
- fun `getAddons - suspends until pending actions are completed`() {
+ fun `getAddons - suspends until pending actions are completed`() = runTestOnMain {
val addon = Addon(
id = "ext1",
installedState = Addon.InstalledState("ext1", "1.0", "", true)
@@ -265,11 +265,9 @@ class AddonManagerTest {
}
val addonsProvider: AddonsProvider = mock()
- runBlocking {
- whenever(addonsProvider.getAvailableAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(listOf(addon))
- WebExtensionSupport.initialize(engine, store)
- WebExtensionSupport.installedExtensions[addon.id] = extension
- }
+ whenever(addonsProvider.getAvailableAddons(anyBoolean(), eq(null), language = anyString())).thenReturn(listOf(addon))
+ WebExtensionSupport.initialize(engine, store)
+ WebExtensionSupport.installedExtensions[addon.id] = extension
val addonManager = AddonManager(store, mock(), addonsProvider, mock())
addonManager.installAddon(addon)
@@ -283,10 +281,8 @@ class AddonManagerTest {
getAddonsResult = addonManager.getAddons(waitForPendingActions = false)
}
- runBlocking {
- nonSuspendingJob.join()
- assertNotNull(getAddonsResult)
- }
+ nonSuspendingJob.join()
+ assertNotNull(getAddonsResult)
getAddonsResult = null
val suspendingJob = CoroutineScope(Dispatchers.IO).launch {
@@ -295,14 +291,12 @@ class AddonManagerTest {
addonManager.pendingAddonActions.forEach { it.complete(Unit) }
- runBlocking {
- suspendingJob.join()
- assertNotNull(getAddonsResult)
- }
+ suspendingJob.join()
+ assertNotNull(getAddonsResult)
}
@Test
- fun `getAddons - passes on allowCache parameter`() = runBlocking {
+ fun `getAddons - passes on allowCache parameter`() = runTestOnMain {
val store = BrowserStore()
val engine: Engine = mock()
diff --git a/components/feature/addons/src/test/java/mozilla/components/feature/addons/amo/AddonCollectionProviderTest.kt b/components/feature/addons/src/test/java/mozilla/components/feature/addons/amo/AddonCollectionProviderTest.kt
index 9ef7128a9a5..8ecd25b40d3 100644
--- a/components/feature/addons/src/test/java/mozilla/components/feature/addons/amo/AddonCollectionProviderTest.kt
+++ b/components/feature/addons/src/test/java/mozilla/components/feature/addons/amo/AddonCollectionProviderTest.kt
@@ -6,7 +6,7 @@ package mozilla.components.feature.addons.amo
import android.graphics.Bitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.Request
import mozilla.components.concept.fetch.Response
@@ -37,7 +37,7 @@ import java.util.concurrent.TimeUnit
class AddonCollectionProviderTest {
@Test
- fun `getAvailableAddons - with a successful status response must contain add-ons`() = runBlocking {
+ fun `getAvailableAddons - with a successful status response must contain add-ons`() = runTest {
val mockedClient = prepareClient(loadResourceAsString("/collection.json"))
val provider = AddonCollectionProvider(testContext, client = mockedClient)
val addons = provider.getAvailableAddons()
@@ -96,7 +96,7 @@ class AddonCollectionProviderTest {
}
@Test
- fun `getAvailableAddons - with a successful status response must handle empty values`() = runBlocking {
+ fun `getAvailableAddons - with a successful status response must handle empty values`() = runTest {
val client = prepareClient()
val provider = AddonCollectionProvider(testContext, client = client)
@@ -135,7 +135,7 @@ class AddonCollectionProviderTest {
}
@Test
- fun `getAvailableAddons - with a language`() = runBlocking {
+ fun `getAvailableAddons - with a language`() = runTest {
val client = prepareClient(loadResourceAsString("/localized_collection.json"))
val provider = AddonCollectionProvider(testContext, client = client)
@@ -208,7 +208,7 @@ class AddonCollectionProviderTest {
}
@Test
- fun `getAvailableAddons - read timeout can be configured`() = runBlocking {
+ fun `getAvailableAddons - read timeout can be configured`() = runTest {
val mockedClient = prepareClient()
val provider = spy(AddonCollectionProvider(testContext, client = mockedClient))
@@ -224,7 +224,7 @@ class AddonCollectionProviderTest {
}
@Test(expected = IOException::class)
- fun `getAvailableAddons - with unexpected status will throw exception`() = runBlocking {
+ fun `getAvailableAddons - with unexpected status will throw exception`() = runTest {
val mockedClient = prepareClient(status = 500)
val provider = AddonCollectionProvider(testContext, client = mockedClient)
provider.getAvailableAddons()
@@ -232,7 +232,7 @@ class AddonCollectionProviderTest {
}
@Test
- fun `getAvailableAddons - returns cached result if allowed and not expired`() = runBlocking {
+ fun `getAvailableAddons - returns cached result if allowed and not expired`() = runTest {
val mockedClient = prepareClient(loadResourceAsString("/collection.json"))
val provider = spy(AddonCollectionProvider(testContext, client = mockedClient))
@@ -250,7 +250,7 @@ class AddonCollectionProviderTest {
}
@Test
- fun `getAvailableAddons - returns cached result if allowed and fetch failed`() = runBlocking {
+ fun `getAvailableAddons - returns cached result if allowed and fetch failed`() = runTest {
val mockedClient: Client = mock()
val exception = IOException("test")
val cachedAddons: List = emptyList()
@@ -296,7 +296,7 @@ class AddonCollectionProviderTest {
}
@Test
- fun `getAvailableAddons - writes response to cache if configured`() = runBlocking {
+ fun `getAvailableAddons - writes response to cache if configured`() = runTest {
val jsonResponse = loadResourceAsString("/collection.json")
val mockedClient = prepareClient(jsonResponse)
@@ -311,7 +311,7 @@ class AddonCollectionProviderTest {
}
@Test
- fun `getAvailableAddons - deletes unused cache files`() = runBlocking {
+ fun `getAvailableAddons - deletes unused cache files`() = runTest {
val jsonResponse = loadResourceAsString("/collection.json")
val mockedClient = prepareClient(jsonResponse)
@@ -414,7 +414,7 @@ class AddonCollectionProviderTest {
}
@Test
- fun `getAddonIconBitmap - with a successful status will return a bitmap`() = runBlocking {
+ fun `getAddonIconBitmap - with a successful status will return a bitmap`() = runTest {
val mockedClient = mock()
val mockedResponse = mock()
val stream: InputStream = javaClass.getResourceAsStream("/png/mozac.png")!!.buffered()
@@ -441,7 +441,7 @@ class AddonCollectionProviderTest {
}
@Test
- fun `getAddonIconBitmap - with an unsuccessful status will return null`() = runBlocking {
+ fun `getAddonIconBitmap - with an unsuccessful status will return null`() = runTest {
val mockedClient = prepareClient(status = 500)
val provider = AddonCollectionProvider(testContext, client = mockedClient)
val addon = Addon(
@@ -460,7 +460,7 @@ class AddonCollectionProviderTest {
}
@Test
- fun `collection name can be configured`() = runBlocking {
+ fun `collection name can be configured`() = runTest {
val mockedClient = prepareClient()
val collectionName = "collection123"
@@ -483,7 +483,7 @@ class AddonCollectionProviderTest {
}
@Test
- fun `collection sort option can be specified`() = runBlocking {
+ fun `collection sort option can be specified`() = runTest {
val mockedClient = prepareClient()
val collectionName = "collection123"
@@ -593,7 +593,7 @@ class AddonCollectionProviderTest {
}
@Test
- fun `collection user can be configured`() = runBlocking {
+ fun `collection user can be configured`() = runTest {
val mockedClient = prepareClient()
val collectionUser = "user123"
val collectionName = "collection123"
@@ -622,7 +622,7 @@ class AddonCollectionProviderTest {
}
@Test
- fun `default collection is used if not configured`() = runBlocking {
+ fun `default collection is used if not configured`() = runTest {
val mockedClient = prepareClient()
val provider = AddonCollectionProvider(
@@ -645,7 +645,7 @@ class AddonCollectionProviderTest {
}
@Test
- fun `cache file name is sanitized`() = runBlocking {
+ fun `cache file name is sanitized`() = runTest {
val mockedClient = prepareClient()
val collectionUser = "../../user"
val collectionName = "../collection"
diff --git a/components/feature/addons/src/test/java/mozilla/components/feature/addons/menu/WebExtensionActionMenuCandidateTest.kt b/components/feature/addons/src/test/java/mozilla/components/feature/addons/menu/WebExtensionActionMenuCandidateTest.kt
index b47baaa7a25..b3c6863544b 100644
--- a/components/feature/addons/src/test/java/mozilla/components/feature/addons/menu/WebExtensionActionMenuCandidateTest.kt
+++ b/components/feature/addons/src/test/java/mozilla/components/feature/addons/menu/WebExtensionActionMenuCandidateTest.kt
@@ -7,7 +7,7 @@ package mozilla.components.feature.addons.menu
import android.graphics.Color
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.engine.webextension.Action
import mozilla.components.concept.menu.candidate.AsyncDrawableMenuIcon
import mozilla.components.concept.menu.candidate.TextMenuIcon
@@ -97,7 +97,7 @@ class WebExtensionActionMenuCandidateTest {
}
@Test
- fun `create menu candidate with icon`() = runBlockingTest {
+ fun `create menu candidate with icon`() = runTest {
var calledWith: Int = -1
val candidate = baseAction
.copy(
diff --git a/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/DefaultSupportedAddonCheckerTest.kt b/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/DefaultSupportedAddonCheckerTest.kt
index de88150ea61..54b7369ec27 100644
--- a/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/DefaultSupportedAddonCheckerTest.kt
+++ b/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/DefaultSupportedAddonCheckerTest.kt
@@ -16,14 +16,13 @@ import androidx.work.testing.WorkManagerTestInitHelper
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker.Companion.CHECKER_UNIQUE_PERIODIC_WORK_NAME
import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker.Companion.WORK_TAG_PERIODIC
import mozilla.components.support.base.worker.Frequency
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -36,7 +35,6 @@ import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class DefaultSupportedAddonCheckerTest {
- @ExperimentalCoroutinesApi
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
@@ -59,51 +57,47 @@ class DefaultSupportedAddonCheckerTest {
}
@Test
- fun `registerForChecks - schedule work for future checks`() {
+ fun `registerForChecks - schedule work for future checks`() = runTestOnMain {
val frequency = Frequency(1, TimeUnit.DAYS)
val intent = Intent()
val checker = DefaultSupportedAddonsChecker(context, frequency, intent)
val workId = CHECKER_UNIQUE_PERIODIC_WORK_NAME
- runBlocking {
- val workManger = WorkManager.getInstance(testContext)
- val workData = workManger.getWorkInfosForUniqueWork(workId).await()
+ val workManger = WorkManager.getInstance(testContext)
+ val workData = workManger.getWorkInfosForUniqueWork(workId).await()
- assertTrue(workData.isEmpty())
+ assertTrue(workData.isEmpty())
- checker.registerForChecks()
+ checker.registerForChecks()
- assertExtensionIsRegisteredForChecks()
- assertEquals(intent, SupportedAddonsWorker.onNotificationClickIntent)
- // Cleaning work manager
- workManger.cancelUniqueWork(workId)
- }
+ assertExtensionIsRegisteredForChecks()
+ assertEquals(intent, SupportedAddonsWorker.onNotificationClickIntent)
+ // Cleaning work manager
+ workManger.cancelUniqueWork(workId)
}
@Test
- fun `unregisterForChecks - will remove scheduled work for future checks`() {
+ fun `unregisterForChecks - will remove scheduled work for future checks`() = runTestOnMain {
val frequency = Frequency(1, TimeUnit.DAYS)
val checker = DefaultSupportedAddonsChecker(context, frequency)
val workId = CHECKER_UNIQUE_PERIODIC_WORK_NAME
- runBlocking {
- val workManger = WorkManager.getInstance(testContext)
- var workData = workManger.getWorkInfosForUniqueWork(workId).await()
+ val workManger = WorkManager.getInstance(testContext)
+ var workData = workManger.getWorkInfosForUniqueWork(workId).await()
- assertTrue(workData.isEmpty())
+ assertTrue(workData.isEmpty())
- checker.registerForChecks()
+ checker.registerForChecks()
- assertExtensionIsRegisteredForChecks()
+ assertExtensionIsRegisteredForChecks()
- checker.unregisterForChecks()
+ checker.unregisterForChecks()
- workData = workManger.getWorkInfosForUniqueWork(workId).await()
+ workData = workManger.getWorkInfosForUniqueWork(workId).await()
- assertEquals(WorkInfo.State.CANCELLED, workData.first().state)
- }
+ assertEquals(WorkInfo.State.CANCELLED, workData.first().state)
}
private suspend fun assertExtensionIsRegisteredForChecks() {
diff --git a/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/SupportedAddonsWorkerTest.kt b/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/SupportedAddonsWorkerTest.kt
index 0d3ee4c4c5a..64d6052aa38 100644
--- a/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/SupportedAddonsWorkerTest.kt
+++ b/components/feature/addons/src/test/java/mozilla/components/feature/addons/migration/SupportedAddonsWorkerTest.kt
@@ -10,8 +10,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.work.ListenableWorker
import androidx.work.await
import androidx.work.testing.TestListenableWorkerBuilder
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
import mozilla.components.concept.engine.webextension.EnableSource
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManager
@@ -28,6 +26,7 @@ import mozilla.components.support.test.eq
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import mozilla.components.support.test.whenever
import org.junit.After
import org.junit.Assert.assertEquals
@@ -40,12 +39,10 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify
import java.io.IOException
-import java.lang.Exception
@RunWith(AndroidJUnit4::class)
class SupportedAddonsWorkerTest {
- @ExperimentalCoroutinesApi
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
@@ -60,7 +57,7 @@ class SupportedAddonsWorkerTest {
}
@Test
- fun `doWork - will return Result_success and create a notification when a new supported add-on is found`() {
+ fun `doWork - will return Result_success and create a notification when a new supported add-on is found`() = runTestOnMain {
val addonManager = mock()
val worker = TestListenableWorkerBuilder(testContext).build()
var throwable: Throwable? = null
@@ -77,23 +74,21 @@ class SupportedAddonsWorkerTest {
GlobalAddonDependencyProvider.initialize(addonManager, mock(), onCrash)
val onErrorCaptor = argumentCaptor<((Throwable) -> Unit)>()
- runBlocking {
- whenever(addonManager.getAddons()).thenReturn(listOf(unsupportedAddon))
- val result = worker.startWork().await()
+ whenever(addonManager.getAddons()).thenReturn(listOf(unsupportedAddon))
+ val result = worker.startWork().await()
- assertEquals(ListenableWorker.Result.success(), result)
+ assertEquals(ListenableWorker.Result.success(), result)
- val notificationId = SharedIdsHelper.getIdForTag(testContext, NOTIFICATION_TAG)
- assertTrue(isNotificationVisible(notificationId))
- verify(addonManager).enableAddon(eq(unsupportedAddon), source = eq(EnableSource.APP_SUPPORT), onSuccess = any(), onError = onErrorCaptor.capture())
+ val notificationId = SharedIdsHelper.getIdForTag(testContext, NOTIFICATION_TAG)
+ assertTrue(isNotificationVisible(notificationId))
+ verify(addonManager).enableAddon(eq(unsupportedAddon), source = eq(EnableSource.APP_SUPPORT), onSuccess = any(), onError = onErrorCaptor.capture())
- onErrorCaptor.value.invoke(Exception())
- assertNotNull(throwable!!)
- }
+ onErrorCaptor.value.invoke(Exception())
+ assertNotNull(throwable!!)
}
@Test
- fun `doWork - will try pass any exceptions to the crashReporter`() {
+ fun `doWork - will try pass any exceptions to the crashReporter`() = runTestOnMain {
val addonManager = mock()
val worker = TestListenableWorkerBuilder(testContext).build()
var crashWasReported = false
@@ -104,16 +99,14 @@ class SupportedAddonsWorkerTest {
GlobalAddonDependencyProvider.initialize(addonManager, mock(), crashReporter)
GlobalAddonDependencyProvider.addonManager = null
- runBlocking {
- val result = worker.startWork().await()
+ val result = worker.startWork().await()
- assertEquals(ListenableWorker.Result.success(), result)
- assertTrue(crashWasReported)
- }
+ assertEquals(ListenableWorker.Result.success(), result)
+ assertTrue(crashWasReported)
}
@Test
- fun `doWork - will NOT pass any IOExceptions to the crashReporter`() {
+ fun `doWork - will NOT pass any IOExceptions to the crashReporter`() = runTestOnMain {
val addonManager = mock()
val worker = TestListenableWorkerBuilder(testContext).build()
var crashWasReported = false
@@ -123,13 +116,11 @@ class SupportedAddonsWorkerTest {
GlobalAddonDependencyProvider.initialize(addonManager, mock(), crashReporter)
- runBlocking {
- whenever(addonManager.getAddons()).thenThrow(AddonManagerException(IOException()))
- val result = worker.startWork().await()
+ whenever(addonManager.getAddons()).thenThrow(AddonManagerException(IOException()))
+ val result = worker.startWork().await()
- assertEquals(ListenableWorker.Result.success(), result)
- assertFalse(crashWasReported)
- }
+ assertEquals(ListenableWorker.Result.success(), result)
+ assertFalse(crashWasReported)
}
@Test
diff --git a/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonInstallationDialogFragmentTest.kt b/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonInstallationDialogFragmentTest.kt
index 194d2755693..7d76b891d40 100644
--- a/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonInstallationDialogFragmentTest.kt
+++ b/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonInstallationDialogFragmentTest.kt
@@ -15,8 +15,7 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.Job
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.runTest
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.R
import mozilla.components.feature.addons.amo.AddonCollectionProvider
@@ -46,7 +45,7 @@ class AddonInstallationDialogFragmentTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val scope = TestCoroutineScope(coroutinesTestRule.testDispatcher)
+ private val scope = coroutinesTestRule.scope
@Test
fun `build dialog`() {
@@ -142,39 +141,35 @@ class AddonInstallationDialogFragmentTest {
}
@Test
- fun `fetching the add-on icon successfully`() {
+ fun `fetching the add-on icon successfully`() = runTest {
val addon = mock()
val bitmap = mock()
val mockedImageView = spy(ImageView(testContext))
val mockedCollectionProvider = mock()
val fragment = createAddonInstallationDialogFragment(addon, mockedCollectionProvider)
- runBlocking {
- whenever(mockedCollectionProvider.getAddonIconBitmap(addon)).thenReturn(bitmap)
- assertNull(fragment.arguments?.getParcelable(KEY_ICON))
- fragment.fetchIcon(addon, mockedImageView, scope).join()
- assertNotNull(fragment.arguments?.getParcelable(KEY_ICON))
- verify(mockedImageView).setImageDrawable(Mockito.any())
- }
+ whenever(mockedCollectionProvider.getAddonIconBitmap(addon)).thenReturn(bitmap)
+ assertNull(fragment.arguments?.getParcelable(KEY_ICON))
+ fragment.fetchIcon(addon, mockedImageView, scope).join()
+ assertNotNull(fragment.arguments?.getParcelable(KEY_ICON))
+ verify(mockedImageView).setImageDrawable(Mockito.any())
}
@Test
- fun `handle errors while fetching the add-on icon`() {
+ fun `handle errors while fetching the add-on icon`() = runTest {
val addon = mock()
val mockedImageView = spy(ImageView(testContext))
val mockedCollectionProvider = mock()
val fragment = createAddonInstallationDialogFragment(addon, mockedCollectionProvider)
- runBlocking {
- whenever(mockedCollectionProvider.getAddonIconBitmap(addon)).then {
- throw IOException("Request failed")
- }
- try {
- fragment.fetchIcon(addon, mockedImageView, scope).join()
- verify(mockedImageView).setColorFilter(Mockito.anyInt())
- } catch (e: IOException) {
- fail("The exception must be handle in the adapter")
- }
+ whenever(mockedCollectionProvider.getAddonIconBitmap(addon)).then {
+ throw IOException("Request failed")
+ }
+ try {
+ fragment.fetchIcon(addon, mockedImageView, scope).join()
+ verify(mockedImageView).setColorFilter(Mockito.anyInt())
+ } catch (e: IOException) {
+ fail("The exception must be handle in the adapter")
}
}
diff --git a/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonsManagerAdapterTest.kt b/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonsManagerAdapterTest.kt
index eb367d5cf9c..8bca0c169c5 100644
--- a/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonsManagerAdapterTest.kt
+++ b/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/AddonsManagerAdapterTest.kt
@@ -16,7 +16,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.R
import mozilla.components.feature.addons.amo.AddonCollectionProvider
@@ -27,6 +26,7 @@ import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import mozilla.components.support.test.whenever
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -50,7 +50,7 @@ class AddonsManagerAdapterTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val scope = TestCoroutineScope(coroutinesTestRule.testDispatcher)
+ private val scope = coroutinesTestRule.scope
@Test
fun `createListWithSections`() {
@@ -116,64 +116,58 @@ class AddonsManagerAdapterTest {
}
@Test
- fun `handle errors while fetching the add-on icon`() {
+ fun `handle errors while fetching the add-on icon`() = runTestOnMain {
val addon = mock()
val mockedImageView = spy(ImageView(testContext))
val mockedCollectionProvider = mock()
val adapter = AddonsManagerAdapter(mockedCollectionProvider, mock(), emptyList())
+ whenever(mockedCollectionProvider.getAddonIconBitmap(addon)).then {
+ throw IOException("Request failed")
+ }
- runBlocking {
- whenever(mockedCollectionProvider.getAddonIconBitmap(addon)).then {
- throw IOException("Request failed")
- }
- try {
- adapter.fetchIcon(addon, mockedImageView, scope).join()
- verify(mockedImageView).setColorFilter(anyInt())
- } catch (e: IOException) {
- fail("The exception must be handle in the adapter")
- }
+ try {
+ adapter.fetchIcon(addon, mockedImageView, scope).join()
+ verify(mockedImageView).setColorFilter(anyInt())
+ } catch (e: IOException) {
+ fail("The exception must be handle in the adapter")
}
}
@Test
- fun `fetching the add-on icon from cache MUST NOT animate`() {
+ fun `fetching the add-on icon from cache MUST NOT animate`() = runTestOnMain {
val addon = mock()
val bitmap = mock()
val mockedImageView = spy(ImageView(testContext))
val mockedCollectionProvider = mock()
val adapter = AddonsManagerAdapter(mockedCollectionProvider, mock(), emptyList())
+ whenever(mockedCollectionProvider.getAddonIconBitmap(addon)).thenReturn(bitmap)
- runBlocking {
- whenever(mockedCollectionProvider.getAddonIconBitmap(addon)).thenReturn(bitmap)
+ adapter.fetchIcon(addon, mockedImageView, scope).join()
- adapter.fetchIcon(addon, mockedImageView, scope).join()
- verify(mockedImageView).setImageDrawable(any())
- }
+ verify(mockedImageView).setImageDrawable(any())
}
@Test
- fun `fetching the add-on icon uncached MUST animate`() {
+ fun `fetching the add-on icon uncached MUST animate`() = runTestOnMain {
val addon = mock()
val bitmap = mock()
val mockedImageView = spy(ImageView(testContext))
val mockedCollectionProvider = mock()
val adapter = spy(AddonsManagerAdapter(mockedCollectionProvider, mock(), emptyList()))
-
- runBlocking {
- whenever(mockedCollectionProvider.getAddonIconBitmap(addon)).thenAnswer {
- runBlocking {
- delay(1000)
- }
- bitmap
+ whenever(mockedCollectionProvider.getAddonIconBitmap(addon)).thenAnswer {
+ runBlocking {
+ delay(1000)
}
-
- adapter.fetchIcon(addon, mockedImageView, scope).join()
- verify(adapter).setWithCrossFadeAnimation(mockedImageView, bitmap)
+ bitmap
}
+
+ adapter.fetchIcon(addon, mockedImageView, scope).join()
+
+ verify(adapter).setWithCrossFadeAnimation(mockedImageView, bitmap)
}
@Test
- fun `fall back to icon of installed extension`() {
+ fun `fall back to icon of installed extension`() = runTestOnMain {
val addon = mock()
val installedState = mock()
val icon = mock()
@@ -182,14 +176,13 @@ class AddonsManagerAdapterTest {
val mockedImageView = spy(ImageView(testContext))
val mockedCollectionProvider = mock()
val adapter = AddonsManagerAdapter(mockedCollectionProvider, mock(), emptyList())
+ val captor = argumentCaptor()
+ whenever(mockedCollectionProvider.getAddonIconBitmap(addon)).thenReturn(null)
- runBlocking {
- whenever(mockedCollectionProvider.getAddonIconBitmap(addon)).thenReturn(null)
- adapter.fetchIcon(addon, mockedImageView, scope).join()
- val captor = argumentCaptor()
- verify(mockedImageView).setImageDrawable(captor.capture())
- assertEquals(icon, captor.value.bitmap)
- }
+ adapter.fetchIcon(addon, mockedImageView, scope).join()
+
+ verify(mockedImageView).setImageDrawable(captor.capture())
+ assertEquals(icon, captor.value.bitmap)
}
@Test
diff --git a/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapterTest.kt b/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapterTest.kt
index 9e958f30aea..0b3f412aa53 100644
--- a/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapterTest.kt
+++ b/components/feature/addons/src/test/java/mozilla/components/feature/addons/ui/UnsupportedAddonsAdapterTest.kt
@@ -31,7 +31,6 @@ class UnsupportedAddonsAdapterTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val testDispatcher = coroutinesTestRule.testDispatcher
@Test
fun `removing successfully notifies the adapter item changed`() {
@@ -50,19 +49,16 @@ class UnsupportedAddonsAdapterTest {
)
adapter.removeUninstalledAddon(addonOne)
- testDispatcher.advanceUntilIdle()
verify(unsupportedAddonsAdapterDelegate, times(1)).onUninstallSuccess()
verify(adapter, times(1)).notifyDataSetChanged()
assertEquals(1, adapter.itemCount)
adapter.removeUninstalledAddon(addonTwo)
- testDispatcher.advanceUntilIdle()
verify(unsupportedAddonsAdapterDelegate, times(2)).onUninstallSuccess()
verify(adapter, times(2)).notifyDataSetChanged()
assertEquals(0, adapter.itemCount)
adapter.removeUninstalledAddon(addonTwo)
- testDispatcher.advanceUntilIdle()
verify(unsupportedAddonsAdapterDelegate, times(2)).onUninstallSuccess()
verify(adapter, times(2)).notifyDataSetChanged()
}
@@ -116,7 +112,6 @@ class UnsupportedAddonsAdapterTest {
assertFalse(removeButtonOne.isEnabled)
assertFalse(removeButtonTwo.isEnabled)
- testDispatcher.advanceUntilIdle()
verify(addonManager).uninstallAddon(any(), onSuccessCaptor.capture(), any())
onSuccessCaptor.value.invoke()
assertFalse(adapter.pendingUninstall)
diff --git a/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/AddonUpdaterWorkerTest.kt b/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/AddonUpdaterWorkerTest.kt
index f1322d9628d..4056e122a80 100644
--- a/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/AddonUpdaterWorkerTest.kt
+++ b/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/AddonUpdaterWorkerTest.kt
@@ -10,8 +10,6 @@ import androidx.work.await
import androidx.work.testing.TestListenableWorkerBuilder
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.webextension.WebExtension
@@ -22,6 +20,7 @@ import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import mozilla.components.support.test.whenever
import mozilla.components.support.webextensions.WebExtensionSupport
import org.junit.After
@@ -38,7 +37,6 @@ import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class AddonUpdaterWorkerTest {
- @ExperimentalCoroutinesApi
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
@@ -67,7 +65,7 @@ class AddonUpdaterWorkerTest {
}
@Test
- fun `doWork - will return Result_success when SuccessfullyUpdated`() {
+ fun `doWork - will return Result_success when SuccessfullyUpdated`() = runTestOnMain {
val updateAttemptStorage = mock()
val addonId = "addonId"
val onFinishCaptor = argumentCaptor<((AddonUpdater.Status) -> Unit)>()
@@ -85,18 +83,16 @@ class AddonUpdaterWorkerTest {
onFinishCaptor.value.invoke(AddonUpdater.Status.SuccessfullyUpdated)
}
- runBlocking {
- doReturn(this).`when`(worker).attemptScope
+ doReturn(this).`when`(worker).attemptScope
- val result = worker.startWork().await()
+ val result = worker.startWork().await()
- assertEquals(ListenableWorker.Result.success(), result)
- verify(worker).saveUpdateAttempt(addonId, AddonUpdater.Status.SuccessfullyUpdated)
- }
+ assertEquals(ListenableWorker.Result.success(), result)
+ verify(worker).saveUpdateAttempt(addonId, AddonUpdater.Status.SuccessfullyUpdated)
}
@Test
- fun `doWork - will return Result_success when NoUpdateAvailable`() {
+ fun `doWork - will return Result_success when NoUpdateAvailable`() = runTestOnMain {
val addonId = "addonId"
val onFinishCaptor = argumentCaptor<((AddonUpdater.Status) -> Unit)>()
val addonManager = mock()
@@ -110,15 +106,13 @@ class AddonUpdaterWorkerTest {
onFinishCaptor.value.invoke(AddonUpdater.Status.NoUpdateAvailable)
}
- runBlocking {
- val result = worker.startWork().await()
+ val result = worker.startWork().await()
- assertEquals(ListenableWorker.Result.success(), result)
- }
+ assertEquals(ListenableWorker.Result.success(), result)
}
@Test
- fun `doWork - will return Result_failure when NotInstalled`() {
+ fun `doWork - will return Result_failure when NotInstalled`() = runTestOnMain {
val addonId = "addonId"
val onFinishCaptor = argumentCaptor<((AddonUpdater.Status) -> Unit)>()
val addonManager = mock()
@@ -132,15 +126,13 @@ class AddonUpdaterWorkerTest {
onFinishCaptor.value.invoke(AddonUpdater.Status.NotInstalled)
}
- runBlocking {
- val result = worker.startWork().await()
+ val result = worker.startWork().await()
- assertEquals(ListenableWorker.Result.failure(), result)
- }
+ assertEquals(ListenableWorker.Result.failure(), result)
}
@Test
- fun `doWork - will return Result_retry when an Error happens and is recoverable`() {
+ fun `doWork - will return Result_retry when an Error happens and is recoverable`() = runTestOnMain {
val updateAttemptStorage = mock()
val addonId = "addonId"
val onFinishCaptor = argumentCaptor<((AddonUpdater.Status) -> Unit)>()
@@ -157,16 +149,14 @@ class AddonUpdaterWorkerTest {
onFinishCaptor.value.invoke(AddonUpdater.Status.Error("error", recoverableException))
}
- runBlocking {
- val result = worker.startWork().await()
+ val result = worker.startWork().await()
- assertEquals(ListenableWorker.Result.retry(), result)
- updateAttemptStorage.saveOrUpdate(any())
- }
+ assertEquals(ListenableWorker.Result.retry(), result)
+ updateAttemptStorage.saveOrUpdate(any())
}
@Test
- fun `doWork - will return Result_success when an Error happens and is unrecoverable`() {
+ fun `doWork - will return Result_success when an Error happens and is unrecoverable`() = runTestOnMain {
val updateAttemptStorage = mock()
val addonId = "addonId"
val onFinishCaptor = argumentCaptor<((AddonUpdater.Status) -> Unit)>()
@@ -183,16 +173,14 @@ class AddonUpdaterWorkerTest {
onFinishCaptor.value.invoke(AddonUpdater.Status.Error("error", unrecoverableException))
}
- runBlocking {
- val result = worker.startWork().await()
+ val result = worker.startWork().await()
- assertEquals(ListenableWorker.Result.success(), result)
- updateAttemptStorage.saveOrUpdate(any())
- }
+ assertEquals(ListenableWorker.Result.success(), result)
+ updateAttemptStorage.saveOrUpdate(any())
}
@Test
- fun `doWork - will try pass any exceptions to the crashReporter`() {
+ fun `doWork - will try pass any exceptions to the crashReporter`() = runTestOnMain {
val addonId = "addonId"
val onFinishCaptor = argumentCaptor<((AddonUpdater.Status) -> Unit)>()
val addonManager = mock()
@@ -211,12 +199,10 @@ class AddonUpdaterWorkerTest {
onFinishCaptor.value.invoke(AddonUpdater.Status.Error("error", Exception()))
}
- runBlocking {
- val result = worker.startWork().await()
+ val result = worker.startWork().await()
- assertEquals(ListenableWorker.Result.success(), result)
- assertTrue(crashWasReported)
- }
+ assertEquals(ListenableWorker.Result.success(), result)
+ assertTrue(crashWasReported)
}
@Test
diff --git a/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/DefaultAddonUpdaterTest.kt b/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/DefaultAddonUpdaterTest.kt
index 1c17497e888..7ff86c495ec 100644
--- a/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/DefaultAddonUpdaterTest.kt
+++ b/components/feature/addons/src/test/java/mozilla/components/feature/addons/update/DefaultAddonUpdaterTest.kt
@@ -19,8 +19,6 @@ import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
import mozilla.components.concept.engine.webextension.DisabledFlags
import mozilla.components.concept.engine.webextension.Metadata
import mozilla.components.concept.engine.webextension.WebExtension
@@ -32,6 +30,7 @@ import mozilla.components.support.base.worker.Frequency
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import mozilla.components.support.test.whenever
import org.junit.Before
import org.junit.Rule
@@ -46,7 +45,6 @@ import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class DefaultAddonUpdaterTest {
- @ExperimentalCoroutinesApi
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
@@ -59,58 +57,54 @@ class DefaultAddonUpdaterTest {
}
@Test
- fun `registerForFutureUpdates - schedule work for future update`() {
+ fun `registerForFutureUpdates - schedule work for future update`() = runTestOnMain {
val frequency = Frequency(1, TimeUnit.DAYS)
val updater = DefaultAddonUpdater(testContext, frequency)
val addonId = "addonId"
val workId = updater.getUniquePeriodicWorkName(addonId)
- runBlocking {
- val workManger = WorkManager.getInstance(testContext)
- var workData = workManger.getWorkInfosForUniqueWork(workId).await()
+ val workManger = WorkManager.getInstance(testContext)
+ var workData = workManger.getWorkInfosForUniqueWork(workId).await()
- assertTrue(workData.isEmpty())
+ assertTrue(workData.isEmpty())
- updater.registerForFutureUpdates(addonId)
- workData = workManger.getWorkInfosForUniqueWork(workId).await()
+ updater.registerForFutureUpdates(addonId)
+ workData = workManger.getWorkInfosForUniqueWork(workId).await()
- assertFalse(workData.isEmpty())
+ assertFalse(workData.isEmpty())
- assertExtensionIsRegisteredFoUpdates(updater, addonId)
+ assertExtensionIsRegisteredFoUpdates(updater, addonId)
- // Cleaning work manager
- workManger.cancelUniqueWork(workId)
- }
+ // Cleaning work manager
+ workManger.cancelUniqueWork(workId)
}
@Test
- fun `update - schedule work for immediate update`() {
+ fun `update - schedule work for immediate update`() = runTestOnMain {
val updater = DefaultAddonUpdater(testContext)
val addonId = "addonId"
val workId = updater.getUniqueImmediateWorkName(addonId)
- runBlocking {
- val workManger = WorkManager.getInstance(testContext)
- var workData = workManger.getWorkInfosForUniqueWork(workId).await()
+ val workManger = WorkManager.getInstance(testContext)
+ var workData = workManger.getWorkInfosForUniqueWork(workId).await()
- assertTrue(workData.isEmpty())
+ assertTrue(workData.isEmpty())
- updater.update(addonId)
- workData = workManger.getWorkInfosForUniqueWork(workId).await()
+ updater.update(addonId)
+ workData = workManger.getWorkInfosForUniqueWork(workId).await()
- assertFalse(workData.isEmpty())
+ assertFalse(workData.isEmpty())
- val work = workData.first()
+ val work = workData.first()
- assertEquals(WorkInfo.State.ENQUEUED, work.state)
- assertTrue(work.tags.contains(workId))
- assertTrue(work.tags.contains(WORK_TAG_IMMEDIATE))
+ assertEquals(WorkInfo.State.ENQUEUED, work.state)
+ assertTrue(work.tags.contains(workId))
+ assertTrue(work.tags.contains(WORK_TAG_IMMEDIATE))
- // Cleaning work manager
- workManger.cancelUniqueWork(workId)
- }
+ // Cleaning work manager
+ workManger.cancelUniqueWork(workId)
}
@Test
@@ -264,7 +258,7 @@ class DefaultAddonUpdaterTest {
}
@Test
- fun `unregisterForFutureUpdates - will remove scheduled work for future update`() {
+ fun `unregisterForFutureUpdates - will remove scheduled work for future update`() = runTestOnMain {
val frequency = Frequency(1, TimeUnit.DAYS)
val updater = DefaultAddonUpdater(testContext, frequency)
updater.scope = CoroutineScope(Dispatchers.Main)
@@ -275,25 +269,23 @@ class DefaultAddonUpdaterTest {
val workId = updater.getUniquePeriodicWorkName(addonId)
- runBlocking {
- val workManger = WorkManager.getInstance(testContext)
- var workData = workManger.getWorkInfosForUniqueWork(workId).await()
+ val workManger = WorkManager.getInstance(testContext)
+ var workData = workManger.getWorkInfosForUniqueWork(workId).await()
- assertTrue(workData.isEmpty())
+ assertTrue(workData.isEmpty())
- updater.registerForFutureUpdates(addonId)
- workData = workManger.getWorkInfosForUniqueWork(workId).await()
+ updater.registerForFutureUpdates(addonId)
+ workData = workManger.getWorkInfosForUniqueWork(workId).await()
- assertFalse(workData.isEmpty())
+ assertFalse(workData.isEmpty())
- assertExtensionIsRegisteredFoUpdates(updater, addonId)
+ assertExtensionIsRegisteredFoUpdates(updater, addonId)
- updater.unregisterForFutureUpdates(addonId)
+ updater.unregisterForFutureUpdates(addonId)
- workData = workManger.getWorkInfosForUniqueWork(workId).await()
- assertEquals(WorkInfo.State.CANCELLED, workData.first().state)
- verify(updater.updateAttempStorage).remove(addonId)
- }
+ workData = workManger.getWorkInfosForUniqueWork(workId).await()
+ assertEquals(WorkInfo.State.CANCELLED, workData.first().state)
+ verify(updater.updateAttempStorage).remove(addonId)
}
@Test
@@ -315,7 +307,7 @@ class DefaultAddonUpdaterTest {
}
@Test
- fun `registerForFutureUpdates - will register only unregistered extensions`() {
+ fun `registerForFutureUpdates - will register only unregistered extensions`() = runTestOnMain {
val updater = DefaultAddonUpdater(testContext)
val registeredExt: WebExtension = mock()
val notRegisteredExt: WebExtension = mock()
@@ -326,21 +318,17 @@ class DefaultAddonUpdaterTest {
val extensions = listOf(registeredExt, notRegisteredExt)
- runBlocking {
- assertExtensionIsRegisteredFoUpdates(updater, "registeredExt")
- }
+ assertExtensionIsRegisteredFoUpdates(updater, "registeredExt")
updater.registerForFutureUpdates(extensions)
- runBlocking {
- extensions.forEach { ext ->
- assertExtensionIsRegisteredFoUpdates(updater, ext.id)
- }
+ extensions.forEach { ext ->
+ assertExtensionIsRegisteredFoUpdates(updater, ext.id)
}
}
@Test
- fun `registerForFutureUpdates - will not register built-in and unsupported extensions`() {
+ fun `registerForFutureUpdates - will not register built-in and unsupported extensions`() = runTestOnMain {
val updater = DefaultAddonUpdater(testContext)
val regularExt: WebExtension = mock()
@@ -359,14 +347,10 @@ class DefaultAddonUpdaterTest {
val extensions = listOf(regularExt, builtInExt, unsupportedExt)
updater.registerForFutureUpdates(extensions)
- runBlocking {
- assertExtensionIsRegisteredFoUpdates(updater, regularExt.id)
- }
+ assertExtensionIsRegisteredFoUpdates(updater, regularExt.id)
- runBlocking {
- assertExtensionIsNotRegisteredFoUpdates(updater, builtInExt.id)
- assertExtensionIsNotRegisteredFoUpdates(updater, unsupportedExt.id)
- }
+ assertExtensionIsNotRegisteredFoUpdates(updater, builtInExt.id)
+ assertExtensionIsNotRegisteredFoUpdates(updater, unsupportedExt.id)
}
private suspend fun assertExtensionIsRegisteredFoUpdates(updater: DefaultAddonUpdater, extId: String) {
diff --git a/components/feature/app-links/src/main/res/values-ast/strings.xml b/components/feature/app-links/src/main/res/values-ast/strings.xml
index e42b6550310..62205d808ad 100644
--- a/components/feature/app-links/src/main/res/values-ast/strings.xml
+++ b/components/feature/app-links/src/main/res/values-ast/strings.xml
@@ -2,8 +2,6 @@
Abrir en…
-
- ¿Abrir na aplicación? La to actividá yá nun va ser privada.Abrir
diff --git a/components/feature/app-links/src/main/res/values-skr/strings.xml b/components/feature/app-links/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..4db30a340e6
--- /dev/null
+++ b/components/feature/app-links/src/main/res/values-skr/strings.xml
@@ -0,0 +1,11 @@
+
+
+
+ ۔۔۔ وچ کھولو
+
+ ایپ وچ کھولوں؟ تہاݙی سرگرمی ہݨ نجی کائناں ہوسی۔
+
+ کھولو
+
+ منسوخ
+
diff --git a/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksFeatureTest.kt b/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksFeatureTest.kt
index 2af798f755b..ccdf715d438 100644
--- a/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksFeatureTest.kt
+++ b/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksFeatureTest.kt
@@ -45,7 +45,6 @@ class AppLinksFeatureTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val testDispatcher = coroutinesTestRule.testDispatcher
private lateinit var store: BrowserStore
private lateinit var mockContext: Context
@@ -120,7 +119,6 @@ class AppLinksFeatureTest {
val tabWithPendingAppIntent = store.state.findTab(tab.id)!!
assertNotNull(tabWithPendingAppIntent.content.appIntent)
- testDispatcher.advanceUntilIdle()
verify(feature).handleAppIntent(tabWithPendingAppIntent, intentUrl, intent)
store.waitUntilIdle()
@@ -139,7 +137,7 @@ class AppLinksFeatureTest {
val intent: Intent = mock()
val appIntent = AppIntentState(intentUrl, intent)
store.dispatch(ContentAction.UpdateAppIntentAction(tab.id, appIntent)).joinBlocking()
- testDispatcher.advanceUntilIdle()
+
verify(feature, never()).handleAppIntent(any(), any(), any())
}
diff --git a/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt b/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt
index dffaafe7821..694dd0c6bd8 100644
--- a/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt
+++ b/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksUseCasesTest.kt
@@ -12,11 +12,9 @@ import android.content.pm.ActivityInfo
import android.content.pm.PackageInfo
import android.content.pm.ResolveInfo
import android.net.Uri
+import android.os.Looper.getMainLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
@@ -35,7 +33,7 @@ import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.robolectric.Shadows.shadowOf
import java.io.File
-import java.lang.NullPointerException
+import java.util.concurrent.TimeUnit.MILLISECONDS
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@@ -245,7 +243,10 @@ class AppLinksUseCasesTest {
@Test
fun `A URL that matches only general packages is not an app link`() {
- val context = createContext(Triple(appUrl, browserPackage, ""), Triple(browserUrl, browserPackage, ""))
+ val context = createContext(
+ Triple(appUrl, browserPackage, ""),
+ Triple(browserUrl, browserPackage, "")
+ )
val subject = AppLinksUseCases(context, { true })
val redirect = subject.interceptedAppLinkRedirect(appUrl)
@@ -257,7 +258,11 @@ class AppLinksUseCasesTest {
@Test
fun `A URL that also matches both specialized and general packages is an app link`() {
- val context = createContext(Triple(appUrl, appPackage, ""), Triple(appUrl, browserPackage, ""), Triple(browserUrl, browserPackage, ""))
+ val context = createContext(
+ Triple(appUrl, appPackage, ""),
+ Triple(appUrl, browserPackage, ""),
+ Triple(browserUrl, browserPackage, "")
+ )
val subject = AppLinksUseCases(context, { true })
val redirect = subject.interceptedAppLinkRedirect(appUrl)
@@ -343,7 +348,8 @@ class AppLinksUseCasesTest {
@Test
fun `A intent scheme uri with a fallback without an installed app is not an app link`() {
- val uri = "intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;S.browser_fallback_url=http%3A%2F%2Fzxing.org;end"
+ val uri =
+ "intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;S.browser_fallback_url=http%3A%2F%2Fzxing.org;end"
val context = createContext()
val subject = AppLinksUseCases(context, { true })
@@ -428,27 +434,24 @@ class AppLinksUseCasesTest {
@Test
fun `AppLinksUsecases uses cache`() {
- val testDispatcher = TestCoroutineDispatcher()
- TestCoroutineScope(testDispatcher).launch {
- val context = createContext(Triple(appUrl, appPackage, ""))
-
- var subject = AppLinksUseCases(context, { true })
- var redirect = subject.interceptedAppLinkRedirect(appUrl)
- assertTrue(redirect.isRedirect())
- val timestamp = AppLinksUseCases.redirectCache?.cacheTimeStamp
-
- testDispatcher.advanceTimeBy(APP_LINKS_CACHE_INTERVAL / 2)
- subject = AppLinksUseCases(context, { true })
- redirect = subject.interceptedAppLinkRedirect(appUrl)
- assertTrue(redirect.isRedirect())
- assert(timestamp == AppLinksUseCases.redirectCache?.cacheTimeStamp)
-
- testDispatcher.advanceTimeBy(APP_LINKS_CACHE_INTERVAL / 2 + 1)
- subject = AppLinksUseCases(context, { true })
- redirect = subject.interceptedAppLinkRedirect(appUrl)
- assertTrue(redirect.isRedirect())
- assert(timestamp != AppLinksUseCases.redirectCache?.cacheTimeStamp)
- }
+ val context = createContext(Triple(appUrl, appPackage, ""))
+
+ var subject = AppLinksUseCases(context, { true })
+ var redirect = subject.interceptedAppLinkRedirect(appUrl)
+ assertTrue(redirect.isRedirect())
+ val timestamp = AppLinksUseCases.redirectCache?.cacheTimeStamp
+
+ shadowOf(getMainLooper()).idleFor(APP_LINKS_CACHE_INTERVAL / 2, MILLISECONDS)
+ subject = AppLinksUseCases(context, { true })
+ redirect = subject.interceptedAppLinkRedirect(appUrl)
+ assertTrue(redirect.isRedirect())
+ assert(timestamp == AppLinksUseCases.redirectCache?.cacheTimeStamp)
+
+ shadowOf(getMainLooper()).idleFor(APP_LINKS_CACHE_INTERVAL / 2 + 1, MILLISECONDS)
+ subject = AppLinksUseCases(context, { true })
+ redirect = subject.interceptedAppLinkRedirect(appUrl)
+ assertTrue(redirect.isRedirect())
+ assert(timestamp != AppLinksUseCases.redirectCache?.cacheTimeStamp)
}
@Test
@@ -591,7 +594,8 @@ class AppLinksUseCasesTest {
assertNull(result)
- uri = "intent://blank#Intent;package=test;i.android.support.customtabs.extra.TOOLBAR_COLOR=2239095040;end"
+ uri =
+ "intent://blank#Intent;package=test;i.android.support.customtabs.extra.TOOLBAR_COLOR=2239095040;end"
result = subject.safeParseUri(uri, 0)
assertNull(result)
diff --git a/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/AuthFillResponseBuilder.kt b/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/AuthFillResponseBuilder.kt
index d9459f08a77..e71b6416d0b 100644
--- a/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/AuthFillResponseBuilder.kt
+++ b/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/response/fill/AuthFillResponseBuilder.kt
@@ -11,6 +11,7 @@ import android.content.Intent
import android.content.IntentSender
import android.graphics.drawable.Icon
import android.os.Build
+import android.os.Parcel
import android.service.autofill.FillResponse
import android.service.autofill.InlinePresentation
import android.view.autofill.AutofillId
@@ -52,10 +53,21 @@ internal data class AuthFillResponseBuilder(
}
val authIntent = Intent(context, configuration.unlockActivity)
- authIntent.putExtra(
- AbstractAutofillUnlockActivity.EXTRA_PARSED_STRUCTURE,
- parsedStructure
- )
+
+ // Pass `ParsedStructure` as raw bytes to prevent the system throwing a ClassNotFoundException
+ // when updating the PendingIntent and trying to create and remap `ParsedStructure`
+ // from the parcelable extra because of an unknown ClassLoader.
+ with(Parcel.obtain()) {
+ parsedStructure.writeToParcel(this, 0)
+
+ authIntent.putExtra(
+ AbstractAutofillUnlockActivity.EXTRA_PARSED_STRUCTURE,
+ this.marshall()
+ )
+
+ recycle()
+ }
+
authIntent.putExtra(AbstractAutofillUnlockActivity.EXTRA_IME_SPEC, imeSpec)
authIntent.putExtra(
AbstractAutofillUnlockActivity.EXTRA_MAX_SUGGESTION_COUNT,
diff --git a/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ParsedStructure.kt b/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ParsedStructure.kt
index b6c4c4f3f7b..d04c4070e30 100644
--- a/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ParsedStructure.kt
+++ b/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/structure/ParsedStructure.kt
@@ -6,10 +6,11 @@ package mozilla.components.feature.autofill.structure
import android.content.Context
import android.os.Build
+import android.os.Parcel
import android.os.Parcelable
+import android.os.Parcelable.Creator
import android.view.autofill.AutofillId
import androidx.annotation.RequiresApi
-import kotlinx.parcelize.Parcelize
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.utils.Browsers
@@ -20,13 +21,43 @@ import mozilla.components.support.utils.Browsers
* https://github.com/mozilla-lockwise/lockwise-android/blob/d3c0511f73c34e8759e1bb597f2d3dc9bcc146f0/app/src/main/java/mozilla/lockbox/autofill/ParsedStructure.kt#L52
*/
@RequiresApi(Build.VERSION_CODES.O)
-@Parcelize
-internal data class ParsedStructure(
+data class ParsedStructure(
val usernameId: AutofillId? = null,
val passwordId: AutofillId? = null,
val webDomain: String? = null,
val packageName: String
-) : Parcelable
+) : Parcelable {
+ constructor(parcel: Parcel) : this(
+ parcel.readParcelable(AutofillId::class.java.classLoader),
+ parcel.readParcelable(AutofillId::class.java.classLoader),
+ parcel.readString(),
+ parcel.readString() ?: ""
+ )
+
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.writeParcelable(usernameId, flags)
+ parcel.writeParcelable(passwordId, flags)
+ parcel.writeString(webDomain)
+ parcel.writeString(packageName)
+ }
+
+ override fun describeContents(): Int {
+ return 0
+ }
+
+ /**
+ * Create instances of [ParsedStructure] from a [Parcel].
+ */
+ companion object CREATOR : Creator {
+ override fun createFromParcel(parcel: Parcel): ParsedStructure {
+ return ParsedStructure(parcel)
+ }
+
+ override fun newArray(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+ }
+}
/**
* Try to find a domain in the [ParsedStructure] for looking up logins. This is either a "web domain"
diff --git a/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillUnlockActivity.kt b/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillUnlockActivity.kt
index 515b6d5c04c..04b368f55e5 100644
--- a/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillUnlockActivity.kt
+++ b/components/feature/autofill/src/main/java/mozilla/components/feature/autofill/ui/AbstractAutofillUnlockActivity.kt
@@ -7,6 +7,7 @@ package mozilla.components.feature.autofill.ui
import android.content.Intent
import android.os.Build
import android.os.Bundle
+import android.os.Parcel
import android.service.autofill.FillResponse
import android.view.autofill.AutofillManager
import android.widget.inline.InlinePresentationSpec
@@ -39,16 +40,22 @@ abstract class AbstractAutofillUnlockActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val parsedStructure = intent.getParcelableExtra(EXTRA_PARSED_STRUCTURE)
+
+ val parsedStructure = with(Parcel.obtain()) {
+ val rawBytes = intent.getByteArrayExtra(EXTRA_PARSED_STRUCTURE)
+ unmarshall(rawBytes!!, 0, rawBytes.size)
+ setDataPosition(0)
+ ParsedStructure(this).also {
+ recycle()
+ }
+ }
val imeSpec = intent.getImeSpec()
val maxSuggestionCount = intent.getIntExtra(EXTRA_MAX_SUGGESTION_COUNT, MAX_LOGINS)
// While the user is asked to authenticate, we already try to build the fill response asynchronously.
- if (parsedStructure != null) {
- fillResponse = lifecycleScope.async(Dispatchers.IO) {
- val builder = fillHandler.handle(parsedStructure, forceUnlock = true, maxSuggestionCount)
- val result = builder.build(this@AbstractAutofillUnlockActivity, configuration, imeSpec)
- result
- }
+ fillResponse = lifecycleScope.async(Dispatchers.IO) {
+ val builder = fillHandler.handle(parsedStructure, forceUnlock = true, maxSuggestionCount)
+ val result = builder.build(this@AbstractAutofillUnlockActivity, configuration, imeSpec)
+ result
}
if (authenticator == null) {
diff --git a/components/feature/autofill/src/main/res/values-ast/strings.xml b/components/feature/autofill/src/main/res/values-ast/strings.xml
index 85e19d6c869..e4dd33d5aea 100644
--- a/components/feature/autofill/src/main/res/values-ast/strings.xml
+++ b/components/feature/autofill/src/main/res/values-ast/strings.xml
@@ -3,7 +3,7 @@
- Desbloquiar %1$s
+ Desbloquiar «%1$s»
- Contraseña de %1$s
+ Contraseña de: %1$s
@@ -24,7 +24,7 @@
Links" this application is not the official Twitter application for twitter.com credentials.
%1$s will be replaced with the name of the browser application (e.g. Firefox).
-->
- %1$s nun pudo verificar l\'autenticidá de l\'aplicación. ¿Quies siguir col rellenu automáticu de los datos esbillaos?
+ «%1$s» nun pudo verificar l\'autenticidá de l\'aplicación. ¿Quies siguir col rellenu automáticu de los datos esbillaos?
@@ -33,4 +33,13 @@
Non
+
+
+ Buscar «%1$s»
+
+
+ Buscar nes cuentes
diff --git a/components/feature/autofill/src/main/res/values-skr/strings.xml b/components/feature/autofill/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..378959c47cb
--- /dev/null
+++ b/components/feature/autofill/src/main/res/values-skr/strings.xml
@@ -0,0 +1,46 @@
+
+
+
+
+ %1$s اݨ لاک کرو
+
+
+ (ورتݨ ناں کوئی کائنی)
+
+
+ %1$s کیتے پاس ورڈ
+
+
+ پڑتال ناکام تھی ڳئی
+
+
+ %1$s ایپ دے مستند ہووݨ دی تصدیق کائنی کر سڳا۔ بھلا تساں چݨی اسناد کوں خودکاربھرݨ نال اڳوں تے ودھݨ چاہسو؟
+
+
+ جیا
+
+
+ کو
+
+
+ %1$s ڳولو
+
+
+ لاگ ان ڳولو
+
diff --git a/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/handler/FillRequestHandlerTest.kt b/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/handler/FillRequestHandlerTest.kt
index a153d3f0895..21228b564bf 100644
--- a/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/handler/FillRequestHandlerTest.kt
+++ b/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/handler/FillRequestHandlerTest.kt
@@ -4,7 +4,8 @@
package mozilla.components.feature.autofill.handler
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.storage.Login
import mozilla.components.concept.storage.LoginsStorage
import mozilla.components.feature.autofill.AutofillConfiguration
@@ -34,6 +35,7 @@ import org.mockito.Mockito.doReturn
import org.robolectric.RobolectricTestRunner
import java.util.UUID
+@ExperimentalCoroutinesApi // for createTestCase
@RunWith(RobolectricTestRunner::class)
internal class FillRequestHandlerTest {
@Test
@@ -182,13 +184,14 @@ internal class FillRequestHandlerTest {
}
}
+@ExperimentalCoroutinesApi
private fun FillRequestHandlerTest.createTestCase(
filename: String,
packageName: String,
logins: Map,
assertThat: (B?) -> Unit,
canVerifyRelationship: Boolean = true
-) = runBlocking {
+) = runTest {
val structure = createMockStructure(filename, packageName)
val storage: LoginsStorage = mock()
diff --git a/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/structure/ParsedStructureTest.kt b/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/structure/ParsedStructureTest.kt
new file mode 100644
index 00000000000..88504a061b8
--- /dev/null
+++ b/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/structure/ParsedStructureTest.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.feature.autofill.structure
+
+import android.os.Parcel
+import android.view.autofill.AutofillId
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ParsedStructureTest {
+ @Test
+ fun `Given a ParsedStructure WHEN parcelling and unparcelling it THEN get the same object`() {
+ // AutofillId constructor is private but it can be constructed from a parcel.
+ // Use this route instead of mocking to avoid errors like below:
+ // org.robolectric.shadows.ShadowParcel$UnreliableBehaviorError: Looking for Integer at position 72, found String
+ val usernameIdAutofillIdParcel = Parcel.obtain().apply {
+ writeInt(1) // viewId
+ writeInt(3) // flags
+ writeInt(78) // virtualIntId
+ setDataPosition(0) // be a good citizen
+ }
+ val passwordIdAutofillParcel = Parcel.obtain().apply {
+ writeInt(11) // viewId
+ writeInt(31) // flags
+ writeInt(781) // virtualIntId
+ setDataPosition(0) // be a good citizen
+ }
+ val parsedStructure = ParsedStructure(
+ usernameId = AutofillId.CREATOR.createFromParcel(usernameIdAutofillIdParcel),
+ passwordId = AutofillId.CREATOR.createFromParcel(passwordIdAutofillParcel),
+ packageName = "test",
+ webDomain = "https://mozilla.org"
+ )
+
+ // Write the object in a new Parcel.
+ val parcel = Parcel.obtain()
+ parsedStructure.writeToParcel(parcel, 0)
+
+ // Reset Parcel r/w position to be read from beginning afterwards.
+ parcel.setDataPosition(0)
+
+ // Reconstruct the original object from the Parcel.
+ val result = ParsedStructure(parcel)
+
+ assertEquals(parsedStructure, result)
+ }
+}
diff --git a/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/test/MockStructure.kt b/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/test/MockStructure.kt
index 7d5ba35962c..0e349ca56d6 100644
--- a/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/test/MockStructure.kt
+++ b/components/feature/autofill/src/test/java/mozilla/components/feature/autofill/test/MockStructure.kt
@@ -5,11 +5,13 @@
package mozilla.components.feature.autofill.test
import android.view.autofill.AutofillId
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.feature.autofill.handler.FillRequestHandlerTest
import mozilla.components.feature.autofill.structure.AutofillNodeNavigator
import mozilla.components.feature.autofill.structure.RawStructure
import java.io.File
+@ExperimentalCoroutinesApi
internal fun FillRequestHandlerTest.createMockStructure(filename: String, packageName: String): RawStructure {
val classLoader = javaClass.classLoader ?: throw RuntimeException("No class loader")
val resource = classLoader.getResource(filename) ?: throw RuntimeException("Resource not found")
diff --git a/components/feature/awesomebar/build.gradle b/components/feature/awesomebar/build.gradle
index 4a2cced89d0..27a45be29c0 100644
--- a/components/feature/awesomebar/build.gradle
+++ b/components/feature/awesomebar/build.gradle
@@ -49,6 +49,7 @@ dependencies {
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
testImplementation Dependencies.testing_mockwebserver
+ testImplementation Dependencies.testing_coroutines
}
apply from: '../../../publish.gradle'
diff --git a/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProvider.kt b/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProvider.kt
index 787cba269fe..78b210c1f93 100644
--- a/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProvider.kt
+++ b/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProvider.kt
@@ -49,7 +49,7 @@ class CombinedHistorySuggestionProvider(
private val loadUrlUseCase: SessionUseCases.LoadUrlUseCase,
private val icons: BrowserIcons? = null,
internal val engine: Engine? = null,
- @VisibleForTesting internal val maxNumberOfSuggestions: Int = DEFAULT_COMBINED_SUGGESTION_LIMIT,
+ @VisibleForTesting internal var maxNumberOfSuggestions: Int = DEFAULT_COMBINED_SUGGESTION_LIMIT,
private val showEditSuggestion: Boolean = true,
) : AwesomeBar.SuggestionProvider {
override val id: String = UUID.randomUUID().toString()
@@ -95,4 +95,22 @@ class CombinedHistorySuggestionProvider(
return@coroutineScope combinedSuggestions
}
+
+ /**
+ * Set maximum number of suggestions.
+ */
+ fun setMaxNumberOfSuggestions(maxNumber: Int) {
+ if (maxNumber <= 0) {
+ return
+ }
+
+ maxNumberOfSuggestions = maxNumber
+ }
+
+ /**
+ * Reset maximum number of suggestions to default.
+ */
+ fun resetToDefaultMaxSuggestions() {
+ maxNumberOfSuggestions = DEFAULT_COMBINED_SUGGESTION_LIMIT
+ }
}
diff --git a/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProvider.kt b/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProvider.kt
index ce0024d44c1..dcd083d97dc 100644
--- a/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProvider.kt
+++ b/components/feature/awesomebar/src/main/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProvider.kt
@@ -41,7 +41,7 @@ class HistoryStorageSuggestionProvider(
private val loadUrlUseCase: SessionUseCases.LoadUrlUseCase,
private val icons: BrowserIcons? = null,
internal val engine: Engine? = null,
- @VisibleForTesting internal val maxNumberOfSuggestions: Int = DEFAULT_HISTORY_SUGGESTION_LIMIT,
+ @VisibleForTesting internal var maxNumberOfSuggestions: Int = DEFAULT_HISTORY_SUGGESTION_LIMIT,
private val showEditSuggestion: Boolean = true,
) : AwesomeBar.SuggestionProvider {
@@ -63,6 +63,24 @@ class HistoryStorageSuggestionProvider(
return suggestions.into(this, icons, loadUrlUseCase, showEditSuggestion)
}
+
+ /**
+ * Set maximum number of suggestions.
+ */
+ fun setMaxNumberOfSuggestions(maxNumber: Int) {
+ if (maxNumber <= 0) {
+ return
+ }
+
+ maxNumberOfSuggestions = maxNumber
+ }
+
+ /**
+ * Reset maximum number of suggestions to default.
+ */
+ fun resetToDefaultMaxSuggestions() {
+ maxNumberOfSuggestions = DEFAULT_HISTORY_SUGGESTION_LIMIT
+ }
}
internal suspend fun Iterable.into(
diff --git a/components/feature/awesomebar/src/main/res/values-skr/strings.xml b/components/feature/awesomebar/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..31dc0ce0100
--- /dev/null
+++ b/components/feature/awesomebar/src/main/res/values-skr/strings.xml
@@ -0,0 +1,7 @@
+
+
+
+ ٹیب تے ون٘ڄو
+
diff --git a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/BookmarksStorageSuggestionProviderTest.kt b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/BookmarksStorageSuggestionProviderTest.kt
index 6062e3d82ff..34143ac0dce 100644
--- a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/BookmarksStorageSuggestionProviderTest.kt
+++ b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/BookmarksStorageSuggestionProviderTest.kt
@@ -5,7 +5,8 @@
package mozilla.components.feature.awesomebar.provider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.storage.BookmarkInfo
import mozilla.components.concept.storage.BookmarkNode
@@ -25,6 +26,7 @@ import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import java.util.UUID
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class BookmarksStorageSuggestionProviderTest {
@@ -36,7 +38,7 @@ class BookmarksStorageSuggestionProviderTest {
)
@Test
- fun `Provider returns empty list when text is empty`() = runBlocking {
+ fun `Provider returns empty list when text is empty`() = runTest {
val provider = BookmarksStorageSuggestionProvider(mock(), mock())
val suggestions = provider.onInputChanged("")
@@ -44,7 +46,7 @@ class BookmarksStorageSuggestionProviderTest {
}
@Test
- fun `Provider returns suggestions from configured bookmarks storage`() = runBlocking {
+ fun `Provider returns suggestions from configured bookmarks storage`() = runTest {
val provider = BookmarksStorageSuggestionProvider(bookmarks, mock())
val id = bookmarks.addItem("Mobile", newItem.url!!, newItem.title!!, null)
@@ -61,7 +63,7 @@ class BookmarksStorageSuggestionProviderTest {
}
@Test
- fun `Provider does not return duplicate suggestions`() = runBlocking {
+ fun `Provider does not return duplicate suggestions`() = runTest {
val provider = BookmarksStorageSuggestionProvider(bookmarks, mock())
for (i in 1..20) {
@@ -73,7 +75,7 @@ class BookmarksStorageSuggestionProviderTest {
}
@Test
- fun `Provider limits number of returned unique suggestions`() = runBlocking {
+ fun `Provider limits number of returned unique suggestions`() = runTest {
val provider = BookmarksStorageSuggestionProvider(bookmarks, mock())
for (i in 1..100) {
@@ -90,7 +92,7 @@ class BookmarksStorageSuggestionProviderTest {
}
@Test
- fun `provider calls speculative connect for URL of first suggestion`() = runBlocking {
+ fun `provider calls speculative connect for URL of first suggestion`() = runTest {
val engine: Engine = mock()
val provider = BookmarksStorageSuggestionProvider(bookmarks, mock(), engine = engine)
@@ -107,7 +109,7 @@ class BookmarksStorageSuggestionProviderTest {
}
@Test
- fun `WHEN provider is set to not show edit suggestions THEN edit suggestion is set to null`() = runBlocking {
+ fun `WHEN provider is set to not show edit suggestions THEN edit suggestion is set to null`() = runTest {
val engine: Engine = mock()
val provider = BookmarksStorageSuggestionProvider(bookmarks, mock(), engine = engine, showEditSuggestion = false)
diff --git a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/ClipboardSuggestionProviderTest.kt b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/ClipboardSuggestionProviderTest.kt
index ea559c3e8b9..1ead5cffb8d 100644
--- a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/ClipboardSuggestionProviderTest.kt
+++ b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/ClipboardSuggestionProviderTest.kt
@@ -9,7 +9,7 @@ import android.content.ClipboardManager
import android.content.Context
import android.graphics.Bitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.concept.awesomebar.AwesomeBar
import mozilla.components.concept.engine.Engine
import mozilla.components.feature.session.SessionUseCases
@@ -17,10 +17,13 @@ import mozilla.components.support.test.any
import mozilla.components.support.test.eq
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyString
@@ -28,14 +31,18 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
+@ExperimentalCoroutinesApi // for runTestOnMain
@RunWith(AndroidJUnit4::class)
class ClipboardSuggestionProviderTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
private val clipboardManager: ClipboardManager
get() = testContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
@Test
- fun `provider returns empty list by default`() = runBlocking {
+ fun `provider returns empty list by default`() = runTestOnMain {
clipboardManager.clearPrimaryClip()
val provider = ClipboardSuggestionProvider(testContext, mock())
@@ -47,7 +54,7 @@ class ClipboardSuggestionProviderTest {
}
@Test
- fun `provider returns empty list for non plain text clip`() {
+ fun `provider returns empty list for non plain text clip`() = runTestOnMain {
clipboardManager.setPrimaryClip(
ClipData.newHtmlText(
"Label",
@@ -60,7 +67,7 @@ class ClipboardSuggestionProviderTest {
}
@Test
- fun `provider should return suggestion if clipboard contains url`() {
+ fun `provider should return suggestion if clipboard contains url`() = runTestOnMain {
assertClipboardYieldsUrl(
"https://www.mozilla.org",
"https://www.mozilla.org"
@@ -106,11 +113,11 @@ class ClipboardSuggestionProviderTest {
}
@Test
- fun `provider return suggestion on input start`() {
+ fun `provider return suggestion on input start`() = runTestOnMain {
clipboardManager.setPrimaryClip(ClipData.newPlainText("Test label", "https://www.mozilla.org"))
val provider = ClipboardSuggestionProvider(testContext, mock())
- val suggestions = runBlocking { provider.onInputStarted() }
+ val suggestions = provider.onInputStarted()
assertEquals(1, suggestions.size)
@@ -121,14 +128,14 @@ class ClipboardSuggestionProviderTest {
}
@Test
- fun `provider should return no suggestions if clipboard does not contain a url`() {
+ fun `provider should return no suggestions if clipboard does not contain a url`() = runTestOnMain {
assertClipboardYieldsNothing("Hello World")
assertClipboardYieldsNothing("Is this mozilla org")
}
@Test
- fun `provider should allow customization of title and icon on suggestion`() {
+ fun `provider should allow customization of title and icon on suggestion`() = runTestOnMain {
clipboardManager.setPrimaryClip(ClipData.newPlainText("Test label", "http://mozilla.org"))
val bitmap = Bitmap.createBitmap(2, 2, Bitmap.Config.ARGB_8888)
val provider = ClipboardSuggestionProvider(
@@ -139,21 +146,19 @@ class ClipboardSuggestionProviderTest {
requireEmptyText = false
)
- val suggestion = runBlocking {
+ val suggestion = run {
provider.onInputStarted()
val suggestions = provider.onInputChanged("Hello")
suggestions.firstOrNull()
}
- runBlocking {
- assertEquals(bitmap, suggestion?.icon)
- assertEquals("My test title", suggestion?.title)
- }
+ assertEquals(bitmap, suggestion?.icon)
+ assertEquals("My test title", suggestion?.title)
}
@Test
- fun `clicking suggestion loads url`() = runBlocking {
+ fun `clicking suggestion loads url`() = runTestOnMain {
clipboardManager.setPrimaryClip(
ClipData.newPlainText(
"Label",
@@ -180,7 +185,7 @@ class ClipboardSuggestionProviderTest {
}
@Test
- fun `provider returns empty list for non-empty text if empty text required`() = runBlocking {
+ fun `provider returns empty list for non-empty text if empty text required`() = runTestOnMain {
clipboardManager.setPrimaryClip(
ClipData.newPlainText(
"Label",
@@ -194,15 +199,15 @@ class ClipboardSuggestionProviderTest {
}
@Test
- fun `provider calls speculative connect for URL of suggestion`() {
+ fun `provider calls speculative connect for URL of suggestion`() = runTestOnMain {
val engine: Engine = mock()
val provider = ClipboardSuggestionProvider(testContext, mock(), engine = engine)
- var suggestions = runBlocking { provider.onInputStarted() }
+ var suggestions = provider.onInputStarted()
assertTrue(suggestions.isEmpty())
verify(engine, never()).speculativeConnect(anyString())
clipboardManager.setPrimaryClip(ClipData.newPlainText("Test label", "https://www.mozilla.org"))
- suggestions = runBlocking { provider.onInputStarted() }
+ suggestions = provider.onInputStarted()
assertEquals(1, suggestions.size)
verify(engine, times(1)).speculativeConnect(eq("https://www.mozilla.org"))
@@ -211,7 +216,7 @@ class ClipboardSuggestionProviderTest {
assertEquals("https://www.mozilla.org", suggestion.description)
}
- private fun assertClipboardYieldsUrl(text: String, url: String) {
+ private suspend fun assertClipboardYieldsUrl(text: String, url: String) {
val suggestion = getSuggestionWithClipboard(text)
assertNotNull(suggestion)
@@ -219,22 +224,22 @@ class ClipboardSuggestionProviderTest {
assertEquals(url, suggestion!!.description)
}
- private fun assertClipboardYieldsNothing(text: String) {
+ private suspend fun assertClipboardYieldsNothing(text: String) {
val suggestion = getSuggestionWithClipboard(text)
assertNull(suggestion)
}
- private fun getSuggestionWithClipboard(text: String): AwesomeBar.Suggestion? {
+ private suspend fun getSuggestionWithClipboard(text: String): AwesomeBar.Suggestion? {
clipboardManager.setPrimaryClip(ClipData.newPlainText("Test label", text))
return getSuggestion()
}
- private fun getSuggestion(): AwesomeBar.Suggestion? = runBlocking {
+ private suspend fun getSuggestion(): AwesomeBar.Suggestion? {
val provider = ClipboardSuggestionProvider(testContext, mock(), requireEmptyText = false)
provider.onInputStarted()
val suggestions = provider.onInputChanged("Hello")
- suggestions.firstOrNull()
+ return suggestions.firstOrNull()
}
}
diff --git a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProviderTest.kt b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProviderTest.kt
index 5508a1acd32..76f1d19fdcd 100644
--- a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProviderTest.kt
+++ b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/CombinedHistorySuggestionProviderTest.kt
@@ -5,7 +5,8 @@
package mozilla.components.feature.awesomebar.provider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.storage.DocumentType
import mozilla.components.concept.storage.HistoryMetadata
import mozilla.components.concept.storage.HistoryMetadataKey
@@ -22,6 +23,7 @@ import org.junit.runner.RunWith
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doReturn
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class CombinedHistorySuggestionProviderTest {
@@ -36,7 +38,7 @@ class CombinedHistorySuggestionProviderTest {
)
@Test
- fun `GIVEN history items exists WHEN onInputChanged is called with empty text THEN return empty suggestions list`() = runBlocking {
+ fun `GIVEN history items exists WHEN onInputChanged is called with empty text THEN return empty suggestions list`() = runTest {
val metadata: HistoryMetadataStorage = mock()
doReturn(listOf(historyEntry)).`when`(metadata).queryHistoryMetadata(eq("moz"), anyInt())
val history: HistoryStorage = mock()
@@ -48,7 +50,7 @@ class CombinedHistorySuggestionProviderTest {
}
@Test
- fun `GIVEN more suggestions asked than metadata items exist WHEN user changes input THEN return a combined list of suggestions`() = runBlocking {
+ fun `GIVEN more suggestions asked than metadata items exist WHEN user changes input THEN return a combined list of suggestions`() = runTest {
val storage: HistoryMetadataStorage = mock()
doReturn(listOf(historyEntry)).`when`(storage).queryHistoryMetadata(eq("moz"), anyInt())
val history: HistoryStorage = mock()
@@ -63,7 +65,7 @@ class CombinedHistorySuggestionProviderTest {
}
@Test
- fun `GIVEN fewer suggestions asked than metadata items exist WHEN user changes input THEN return suggestions only based on metadata items`() = runBlocking {
+ fun `GIVEN fewer suggestions asked than metadata items exist WHEN user changes input THEN return suggestions only based on metadata items`() = runTest {
val storage: HistoryMetadataStorage = mock()
doReturn(listOf(historyEntry)).`when`(storage).queryHistoryMetadata(eq("moz"), anyInt())
val history: HistoryStorage = mock()
@@ -77,7 +79,7 @@ class CombinedHistorySuggestionProviderTest {
}
@Test
- fun `GIVEN only storage history items exist WHEN user changes input THEN return suggestions only based on storage items`() = runBlocking {
+ fun `GIVEN only storage history items exist WHEN user changes input THEN return suggestions only based on storage items`() = runTest {
val metadata: HistoryMetadataStorage = mock()
doReturn(emptyList()).`when`(metadata).queryHistoryMetadata(eq("moz"), anyInt())
val history: HistoryStorage = mock()
@@ -91,7 +93,7 @@ class CombinedHistorySuggestionProviderTest {
}
@Test
- fun `GIVEN duplicated metadata and storage entries WHEN user changes input THEN return distinct suggestions`() = runBlocking {
+ fun `GIVEN duplicated metadata and storage entries WHEN user changes input THEN return distinct suggestions`() = runTest {
val storage: HistoryMetadataStorage = mock()
doReturn(listOf(historyEntry)).`when`(storage).queryHistoryMetadata(eq("moz"), anyInt())
val history: HistoryStorage = mock()
@@ -105,7 +107,7 @@ class CombinedHistorySuggestionProviderTest {
}
@Test
- fun `GIVEN a combined list of suggestions WHEN history results exist THEN urls are deduped and scores are adjusted`() = runBlocking {
+ fun `GIVEN a combined list of suggestions WHEN history results exist THEN urls are deduped and scores are adjusted`() = runTest {
val metadataEntry1 = HistoryMetadata(
key = HistoryMetadataKey("https://www.mozilla.com", null, null),
title = "mozilla",
@@ -161,7 +163,7 @@ class CombinedHistorySuggestionProviderTest {
}
@Test
- fun `WHEN provider is set to not show edit suggestions THEN edit suggestion is set to null`() = runBlocking {
+ fun `WHEN provider is set to not show edit suggestions THEN edit suggestion is set to null`() = runTest {
val metadata: HistoryMetadataStorage = mock()
doReturn(emptyList()).`when`(metadata).queryHistoryMetadata(eq("moz"), anyInt())
val history: HistoryStorage = mock()
@@ -174,4 +176,59 @@ class CombinedHistorySuggestionProviderTest {
assertEquals("http://www.mozilla.com/firefox/", result[0].description)
assertNull(result[0].editSuggestion)
}
+
+ @Test
+ fun `WHEN provider max number of suggestions is changed THEN the number of return suggestions is updated`() = runTest {
+ val history: HistoryStorage = mock()
+ val metadata: HistoryMetadataStorage = mock()
+ doReturn(emptyList()).`when`(metadata).queryHistoryMetadata(eq("moz"), anyInt())
+ doReturn(
+ (1..50).map {
+ SearchResult("id$it", "http://www.mozilla.com/$it/", 10)
+ }
+ ).`when`(history).getSuggestions(eq("moz"), anyInt())
+
+ val provider = CombinedHistorySuggestionProvider(history, metadata, mock(), showEditSuggestion = false)
+
+ provider.setMaxNumberOfSuggestions(2)
+ var suggestions = provider.onInputChanged("moz")
+ assertEquals(2, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(22)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(22, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(0)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(22, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(45)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(45, suggestions.size)
+ }
+
+ @Test
+ fun `WHEN reset provider max number of suggestions THEN the number of return suggestions is reset to default`() = runTest {
+ val history: HistoryStorage = mock()
+ val metadata: HistoryMetadataStorage = mock()
+ doReturn(emptyList()).`when`(metadata).queryHistoryMetadata(eq("moz"), anyInt())
+ doReturn(
+ (1..50).map {
+ SearchResult("id$it", "http://www.mozilla.com/$it/", 10)
+ }
+ ).`when`(history).getSuggestions(eq("moz"), anyInt())
+
+ val provider = CombinedHistorySuggestionProvider(history, metadata, mock(), showEditSuggestion = false)
+
+ var suggestions = provider.onInputChanged("moz")
+ assertEquals(5, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(45)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(45, suggestions.size)
+
+ provider.resetToDefaultMaxSuggestions()
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(5, suggestions.size)
+ }
}
diff --git a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryMetadataSuggestionProviderTest.kt b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryMetadataSuggestionProviderTest.kt
index 0359457ee54..5790c6b04f7 100644
--- a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryMetadataSuggestionProviderTest.kt
+++ b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryMetadataSuggestionProviderTest.kt
@@ -4,7 +4,8 @@
package mozilla.components.feature.awesomebar.provider
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.storage.DocumentType
import mozilla.components.concept.storage.HistoryMetadata
@@ -30,6 +31,7 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
+@ExperimentalCoroutinesApi // for runTest
class HistoryMetadataSuggestionProviderTest {
private val historyEntry = HistoryMetadata(
key = HistoryMetadataKey("http://www.mozilla.com", null, null),
@@ -47,7 +49,7 @@ class HistoryMetadataSuggestionProviderTest {
}
@Test
- fun `provider returns empty list when text is empty`() = runBlocking {
+ fun `provider returns empty list when text is empty`() = runTest {
val provider = HistoryMetadataSuggestionProvider(mock(), mock())
val suggestions = provider.onInputChanged("")
@@ -55,7 +57,7 @@ class HistoryMetadataSuggestionProviderTest {
}
@Test
- fun `provider returns suggestions from configured history storage`() = runBlocking {
+ fun `provider returns suggestions from configured history storage`() = runTest {
val storage: HistoryMetadataStorage = mock()
whenever(storage.queryHistoryMetadata("moz", DEFAULT_METADATA_SUGGESTION_LIMIT)).thenReturn(listOf(historyEntry))
@@ -68,7 +70,7 @@ class HistoryMetadataSuggestionProviderTest {
}
@Test
- fun `provider limits number of returned suggestions to 5 by default`() = runBlocking {
+ fun `provider limits number of returned suggestions to 5 by default`() = runTest {
val storage: HistoryMetadataStorage = mock()
doReturn(emptyList()).`when`(storage).queryHistoryMetadata(anyString(), anyInt())
val provider = HistoryMetadataSuggestionProvider(storage, mock())
@@ -80,7 +82,7 @@ class HistoryMetadataSuggestionProviderTest {
}
@Test
- fun `provider allows lowering the number of returned suggestions beneath the default`() = runBlocking {
+ fun `provider allows lowering the number of returned suggestions beneath the default`() = runTest {
val storage: HistoryMetadataStorage = mock()
doReturn(emptyList()).`when`(storage).queryHistoryMetadata(anyString(), anyInt())
val provider = HistoryMetadataSuggestionProvider(
@@ -94,7 +96,7 @@ class HistoryMetadataSuggestionProviderTest {
}
@Test
- fun `provider allows increasing the number of returned suggestions above the default`() = runBlocking {
+ fun `provider allows increasing the number of returned suggestions above the default`() = runTest {
val storage: HistoryMetadataStorage = mock()
doReturn(emptyList()).`when`(storage).queryHistoryMetadata(anyString(), anyInt())
val provider = HistoryMetadataSuggestionProvider(
@@ -108,7 +110,7 @@ class HistoryMetadataSuggestionProviderTest {
}
@Test
- fun `provider only as suggestions pages on which users actually spent some time`() = runBlocking {
+ fun `provider only as suggestions pages on which users actually spent some time`() = runTest {
val storage: HistoryMetadataStorage = mock()
val historyEntries = mutableListOf().apply {
add(historyEntry)
@@ -122,7 +124,7 @@ class HistoryMetadataSuggestionProviderTest {
}
@Test
- fun `provider calls speculative connect for URL of highest scored suggestion`() = runBlocking {
+ fun `provider calls speculative connect for URL of highest scored suggestion`() = runTest {
val storage: HistoryMetadataStorage = mock()
val engine: Engine = mock()
val provider = HistoryMetadataSuggestionProvider(storage, mock(), engine = engine)
@@ -139,7 +141,7 @@ class HistoryMetadataSuggestionProviderTest {
}
@Test
- fun `fact is emitted when suggestion is clicked`() = runBlocking {
+ fun `fact is emitted when suggestion is clicked`() = runTest {
val storage: HistoryMetadataStorage = mock()
val engine: Engine = mock()
val provider = HistoryMetadataSuggestionProvider(storage, mock(), engine = engine)
@@ -173,7 +175,7 @@ class HistoryMetadataSuggestionProviderTest {
}
@Test
- fun `WHEN provider is set to not show edit suggestions THEN edit suggestion is set to null`() = runBlocking {
+ fun `WHEN provider is set to not show edit suggestions THEN edit suggestion is set to null`() = runTest {
val storage: HistoryMetadataStorage = mock()
whenever(storage.queryHistoryMetadata("moz", DEFAULT_METADATA_SUGGESTION_LIMIT)).thenReturn(listOf(historyEntry))
diff --git a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProviderTest.kt b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProviderTest.kt
index b82105a4592..c0f91961780 100644
--- a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProviderTest.kt
+++ b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/HistoryStorageSuggestionProviderTest.kt
@@ -5,7 +5,8 @@
package mozilla.components.feature.awesomebar.provider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.concept.storage.SearchResult
@@ -30,6 +31,7 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class HistoryStorageSuggestionProviderTest {
@@ -39,7 +41,7 @@ class HistoryStorageSuggestionProviderTest {
}
@Test
- fun `Provider returns empty list when text is empty`() = runBlocking {
+ fun `Provider returns empty list when text is empty`() = runTest {
val provider = HistoryStorageSuggestionProvider(mock(), mock())
val suggestions = provider.onInputChanged("")
@@ -47,7 +49,7 @@ class HistoryStorageSuggestionProviderTest {
}
@Test
- fun `Provider returns suggestions from configured history storage`() = runBlocking {
+ fun `Provider returns suggestions from configured history storage`() = runTest {
val history: HistoryStorage = mock()
Mockito.doReturn(listOf(SearchResult("id", "http://www.mozilla.com/", 10))).`when`(history).getSuggestions(eq("moz"), Mockito.anyInt())
val provider = HistoryStorageSuggestionProvider(history, mock())
@@ -58,7 +60,7 @@ class HistoryStorageSuggestionProviderTest {
}
@Test
- fun `WHEN provider is set to not show edit suggestions THEN edit suggestion is set to null`() = runBlocking {
+ fun `WHEN provider is set to not show edit suggestions THEN edit suggestion is set to null`() = runTest {
val history: HistoryStorage = mock()
Mockito.doReturn(listOf(SearchResult("id", "http://www.mozilla.com/", 10))).`when`(history).getSuggestions(eq("moz"), Mockito.anyInt())
val provider = HistoryStorageSuggestionProvider(history, mock(), showEditSuggestion = false)
@@ -70,7 +72,7 @@ class HistoryStorageSuggestionProviderTest {
}
@Test
- fun `Provider limits number of returned suggestions to a max of 20 by default`() = runBlocking {
+ fun `Provider limits number of returned suggestions to a max of 20 by default`() = runTest {
val history: HistoryStorage = mock()
Mockito.doReturn(
(1..100).map {
@@ -84,7 +86,7 @@ class HistoryStorageSuggestionProviderTest {
}
@Test
- fun `Provider allows lowering the number of returned suggestions beneath the default`() = runBlocking {
+ fun `Provider allows lowering the number of returned suggestions beneath the default`() = runTest {
val history: HistoryStorage = mock()
Mockito.doReturn(
(1..50).map {
@@ -101,7 +103,7 @@ class HistoryStorageSuggestionProviderTest {
}
@Test
- fun `Provider allows increasing the number of returned suggestions above the default`() = runBlocking {
+ fun `Provider allows increasing the number of returned suggestions above the default`() = runTest {
val history: HistoryStorage = mock()
Mockito.doReturn(
(1..50).map {
@@ -118,7 +120,65 @@ class HistoryStorageSuggestionProviderTest {
}
@Test
- fun `Provider dedupes suggestions`() = runBlocking {
+ fun `WHEN provider max number of suggestions is changed THEN the number of return suggestions is updated`() = runTest {
+ val history: HistoryStorage = mock()
+ Mockito.doReturn(
+ (1..50).map {
+ SearchResult("id$it", "http://www.mozilla.com/$it/", 10)
+ }
+ ).`when`(history).getSuggestions(eq("moz"), Mockito.anyInt())
+
+ val provider = HistoryStorageSuggestionProvider(
+ historyStorage = history, loadUrlUseCase = mock()
+ )
+
+ var suggestions = provider.onInputChanged("moz")
+ assertEquals(20, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(2)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(2, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(22)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(22, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(45)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(45, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(0)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(45, suggestions.size)
+ }
+
+ @Test
+ fun `WHEN reset provider max number of suggestions THEN the number of return suggestions is reset to default`() = runTest {
+ val history: HistoryStorage = mock()
+ Mockito.doReturn(
+ (1..50).map {
+ SearchResult("id$it", "http://www.mozilla.com/$it/", 10)
+ }
+ ).`when`(history).getSuggestions(eq("moz"), Mockito.anyInt())
+
+ val provider = HistoryStorageSuggestionProvider(
+ historyStorage = history, loadUrlUseCase = mock()
+ )
+
+ var suggestions = provider.onInputChanged("moz")
+ assertEquals(20, suggestions.size)
+
+ provider.setMaxNumberOfSuggestions(45)
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(45, suggestions.size)
+
+ provider.resetToDefaultMaxSuggestions()
+ suggestions = provider.onInputChanged("moz")
+ assertEquals(20, suggestions.size)
+ }
+
+ @Test
+ fun `Provider dedupes suggestions`() = runTest {
val storage: HistoryStorage = mock()
val provider = HistoryStorageSuggestionProvider(storage, mock())
@@ -157,7 +217,7 @@ class HistoryStorageSuggestionProviderTest {
}
@Test
- fun `provider calls speculative connect for URL of highest scored suggestion`() = runBlocking {
+ fun `provider calls speculative connect for URL of highest scored suggestion`() = runTest {
val history: HistoryStorage = mock()
val engine: Engine = mock()
val provider = HistoryStorageSuggestionProvider(history, mock(), engine = engine)
@@ -175,7 +235,7 @@ class HistoryStorageSuggestionProviderTest {
}
@Test
- fun `fact is emitted when suggestion is clicked`() = runBlocking {
+ fun `fact is emitted when suggestion is clicked`() = runTest {
val history: HistoryStorage = mock()
val engine: Engine = mock()
val provider = HistoryStorageSuggestionProvider(history, mock(), engine = engine)
diff --git a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchActionProviderTest.kt b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchActionProviderTest.kt
index da2a6d556fd..7a93753d540 100644
--- a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchActionProviderTest.kt
+++ b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchActionProviderTest.kt
@@ -4,36 +4,38 @@
package mozilla.components.feature.awesomebar.provider
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.support.test.mock
import org.junit.Assert.assertEquals
import org.junit.Test
+@ExperimentalCoroutinesApi // for runTest
class SearchActionProviderTest {
@Test
- fun `provider returns no suggestion for empty text`() {
+ fun `provider returns no suggestion for empty text`() = runTest {
val provider = SearchActionProvider(mock(), mock())
- val suggestions = runBlocking { provider.onInputChanged("") }
+ val suggestions = provider.onInputChanged("")
assertEquals(0, suggestions.size)
}
@Test
- fun `provider returns no suggestion for blank text`() {
+ fun `provider returns no suggestion for blank text`() = runTest {
val provider = SearchActionProvider(mock(), mock())
- val suggestions = runBlocking { provider.onInputChanged(" ") }
+ val suggestions = provider.onInputChanged(" ")
assertEquals(0, suggestions.size)
}
@Test
- fun `provider returns suggestion matching input`() {
+ fun `provider returns suggestion matching input`() = runTest {
val provider = SearchActionProvider(
store = mock(),
searchEngine = mock(),
searchUseCase = mock()
)
- val suggestions = runBlocking { provider.onInputChanged("firefox") }
+ val suggestions = provider.onInputChanged("firefox")
assertEquals(1, suggestions.size)
diff --git a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchEngineSuggestionProviderTest.kt b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchEngineSuggestionProviderTest.kt
index 0f628ee1677..557fef99ea0 100644
--- a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchEngineSuggestionProviderTest.kt
+++ b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchEngineSuggestionProviderTest.kt
@@ -6,7 +6,8 @@ package mozilla.components.feature.awesomebar.provider
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.feature.search.ext.createSearchEngine
import mozilla.components.support.test.mock
import mozilla.components.support.test.whenever
@@ -16,6 +17,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class SearchEngineSuggestionProviderTest {
private lateinit var defaultProvider: SearchEngineSuggestionProvider
@@ -43,21 +45,21 @@ class SearchEngineSuggestionProviderTest {
}
@Test
- fun `Provider returns empty list when text is empty`() = runBlocking {
+ fun `Provider returns empty list when text is empty`() = runTest {
val suggestions = defaultProvider.onInputChanged("")
assertTrue(suggestions.isEmpty())
}
@Test
- fun `Provider returns empty list when text is blank`() = runBlocking {
+ fun `Provider returns empty list when text is blank`() = runTest {
val suggestions = defaultProvider.onInputChanged(" ")
assertTrue(suggestions.isEmpty())
}
@Test
- fun `Provider returns empty list when text is shorter than charactersThreshold`() = runBlocking {
+ fun `Provider returns empty list when text is shorter than charactersThreshold`() = runTest {
val provider = SearchEngineSuggestionProvider(
testContext, engineList, mock(), 1, "description", mock(), charactersThreshold = 3
)
@@ -68,7 +70,7 @@ class SearchEngineSuggestionProviderTest {
}
@Test
- fun `Provider returns empty list when list does not contain engines with typed text`() = runBlocking {
+ fun `Provider returns empty list when list does not contain engines with typed text`() = runTest {
val suggestions = defaultProvider.onInputChanged("x")
@@ -76,7 +78,7 @@ class SearchEngineSuggestionProviderTest {
}
@Test
- fun `Provider returns a match when list contains the typed engine`() = runBlocking {
+ fun `Provider returns a match when list contains the typed engine`() = runTest {
val suggestions = defaultProvider.onInputChanged("am")
@@ -84,7 +86,7 @@ class SearchEngineSuggestionProviderTest {
}
@Test
- fun `Provider returns empty list when the engine list is empty`() = runBlocking {
+ fun `Provider returns empty list when the engine list is empty`() = runTest {
val providerEmpty = SearchEngineSuggestionProvider(
testContext, emptyList(), mock(), 1, "description", mock()
)
@@ -95,7 +97,7 @@ class SearchEngineSuggestionProviderTest {
}
@Test
- fun `Provider limits number of returned suggestions to maxSuggestions`() = runBlocking {
+ fun `Provider limits number of returned suggestions to maxSuggestions`() = runTest {
// this should match to both engines in list
val suggestions = defaultProvider.onInputChanged("n")
diff --git a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchSuggestionProviderTest.kt b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchSuggestionProviderTest.kt
index 8c5afc9582d..ec25b82ac12 100644
--- a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchSuggestionProviderTest.kt
+++ b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SearchSuggestionProviderTest.kt
@@ -6,7 +6,8 @@ package mozilla.components.feature.awesomebar.provider
import androidx.core.graphics.drawable.toBitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.Request
@@ -39,11 +40,12 @@ import java.io.IOException
private const val GOOGLE_MOCK_RESPONSE = "[\"firefox\",[\"firefox\",\"firefox for mac\",\"firefox quantum\",\"firefox update\",\"firefox esr\",\"firefox focus\",\"firefox addons\",\"firefox extensions\",\"firefox nightly\",\"firefox clear cache\"]]"
private const val GOOGLE_MOCK_RESPONSE_WITH_DUPLICATES = "[\"firefox\",[\"firefox\",\"firefox\",\"firefox for mac\",\"firefox quantum\",\"firefox update\",\"firefox esr\",\"firefox esr\",\"firefox focus\",\"firefox addons\",\"firefox extensions\",\"firefox nightly\",\"firefox clear cache\"]]"
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class SearchSuggestionProviderTest {
@Test
fun `Provider returns suggestion with chips based on search engine suggestion`() {
- runBlocking {
+ runTest {
val server = MockWebServer()
server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
server.start()
@@ -101,7 +103,7 @@ class SearchSuggestionProviderTest {
@Test
fun `Provider returns multiple suggestions in MULTIPLE mode`() {
- runBlocking {
+ runTest {
val server = MockWebServer()
server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
server.start()
@@ -163,7 +165,7 @@ class SearchSuggestionProviderTest {
@Test
fun `Provider returns multiple suggestions with limit`() {
- runBlocking {
+ runTest {
val server = MockWebServer()
server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
server.start()
@@ -205,7 +207,7 @@ class SearchSuggestionProviderTest {
@Test
fun `Provider returns chips with limit`() {
- runBlocking {
+ runTest {
val server = MockWebServer()
server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
server.start()
@@ -242,7 +244,7 @@ class SearchSuggestionProviderTest {
@Test
fun `Provider should use engine icon by default`() {
- runBlocking {
+ runTest {
val server = MockWebServer()
server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
server.start()
@@ -269,7 +271,7 @@ class SearchSuggestionProviderTest {
@Test
fun `Provider should use icon parameter when available`() {
- runBlocking {
+ runTest {
val server = MockWebServer()
server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
server.start()
@@ -301,7 +303,7 @@ class SearchSuggestionProviderTest {
}
@Test
- fun `Provider returns empty list if text is empty`() = runBlocking {
+ fun `Provider returns empty list if text is empty`() = runTest {
val provider = SearchSuggestionProvider(mock(), mock(), mock())
val suggestions = provider.onInputChanged("")
@@ -310,7 +312,7 @@ class SearchSuggestionProviderTest {
@Test
fun `Provider should return default suggestion for search engine that cannot provide suggestion`() =
- runBlocking {
+ runTest {
val searchEngine = createSearchEngine(
name = "Test",
url = "https://localhost/?q={searchTerms}",
@@ -329,7 +331,7 @@ class SearchSuggestionProviderTest {
@Test
fun `Provider doesn't fail if fetch returns HTTP error`() {
- runBlocking {
+ runTest {
val server = MockWebServer()
server.enqueue(MockResponse().setResponseCode(404).setBody("error"))
server.start()
@@ -361,7 +363,7 @@ class SearchSuggestionProviderTest {
@Test
fun `Provider doesn't fail if fetch throws exception`() {
- runBlocking {
+ runTest {
val searchEngine = createSearchEngine(
name = "Test",
url = "https://localhost/?q={searchTerms}",
@@ -394,7 +396,7 @@ class SearchSuggestionProviderTest {
@Test
fun `Provider returns distinct multiple suggestions`() {
- runBlocking {
+ runTest {
val server = MockWebServer()
server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE_WITH_DUPLICATES))
server.start()
@@ -437,7 +439,7 @@ class SearchSuggestionProviderTest {
@Test
fun `Provider returns multiple suggestions with limit and no description`() {
- runBlocking {
+ runTest {
val server = MockWebServer()
server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
server.start()
@@ -481,7 +483,7 @@ class SearchSuggestionProviderTest {
@Test
fun `Provider calls speculativeConnect for URL of highest scored suggestion in MULTIPLE mode`() {
- runBlocking {
+ runTest {
val server = MockWebServer()
server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
server.start()
@@ -518,7 +520,7 @@ class SearchSuggestionProviderTest {
@Test
fun `Provider calls speculativeConnect for URL of highest scored chip in SINGLE mode`() {
- runBlocking {
+ runTest {
val server = MockWebServer()
server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
server.start()
@@ -555,7 +557,7 @@ class SearchSuggestionProviderTest {
@Test
fun `Provider filters exact match from multiple suggestions`() {
- runBlocking {
+ runTest {
val server = MockWebServer()
server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE_WITH_DUPLICATES))
server.start()
@@ -597,7 +599,7 @@ class SearchSuggestionProviderTest {
@Test
fun `Provider filters chips with exact match`() {
- runBlocking {
+ runTest {
val server = MockWebServer()
server.enqueue(MockResponse().setBody(GOOGLE_MOCK_RESPONSE))
server.start()
diff --git a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SessionSuggestionProviderTest.kt b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SessionSuggestionProviderTest.kt
index 63baaee88f4..f54d0057dac 100644
--- a/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SessionSuggestionProviderTest.kt
+++ b/components/feature/awesomebar/src/test/java/mozilla/components/feature/awesomebar/provider/SessionSuggestionProviderTest.kt
@@ -5,7 +5,8 @@
package mozilla.components.feature.awesomebar.provider
import android.content.res.Resources
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.createTab
@@ -20,9 +21,10 @@ import org.mockito.Mockito.`when`
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+@ExperimentalCoroutinesApi // for runTest
class SessionSuggestionProviderTest {
@Test
- fun `Provider returns empty list when text is empty`() = runBlocking {
+ fun `Provider returns empty list when text is empty`() = runTest {
val resources: Resources = mock()
`when`(resources.getString(anyInt())).thenReturn("Switch to tab")
@@ -33,7 +35,7 @@ class SessionSuggestionProviderTest {
}
@Test
- fun `Provider returns Sessions with matching URLs`() = runBlocking {
+ fun `Provider returns Sessions with matching URLs`() = runTest {
val store = BrowserStore()
val tab1 = createTab("https://www.mozilla.org")
@@ -77,7 +79,7 @@ class SessionSuggestionProviderTest {
}
@Test
- fun `Provider returns Sessions with matching titles`() = runBlocking {
+ fun `Provider returns Sessions with matching titles`() = runTest {
val tab1 = createTab("https://allizom.org", title = "Internet for people, not profit — Mozilla")
val tab2 = createTab("https://getpocket.com", title = "Pocket: My List")
val tab3 = createTab("https://firefox.com", title = "Download Firefox — Free Web Browser")
@@ -113,7 +115,7 @@ class SessionSuggestionProviderTest {
}
@Test
- fun `Provider only returns non-private Sessions`() = runBlocking {
+ fun `Provider only returns non-private Sessions`() = runTest {
val tab = createTab("https://www.mozilla.org")
val privateTab1 = createTab("https://mozilla.org/firefox", private = true)
val privateTab2 = createTab("https://mozilla.org/projects", private = true)
@@ -136,7 +138,7 @@ class SessionSuggestionProviderTest {
}
@Test
- fun `Clicking suggestion invokes SelectTabUseCase`() = runBlocking {
+ fun `Clicking suggestion invokes SelectTabUseCase`() = runTest {
val resources: Resources = mock()
`when`(resources.getString(anyInt())).thenReturn("Switch to tab")
@@ -164,7 +166,7 @@ class SessionSuggestionProviderTest {
}
@Test
- fun `When excludeSelectedSession is true provider should not include the selected session`() = runBlocking {
+ fun `When excludeSelectedSession is true provider should not include the selected session`() = runTest {
val store = BrowserStore(
BrowserState(
tabs = listOf(
@@ -189,7 +191,7 @@ class SessionSuggestionProviderTest {
}
@Test
- fun `When excludeSelectedSession is false provider should include the selected session`() = runBlocking {
+ fun `When excludeSelectedSession is false provider should include the selected session`() = runTest {
val store = BrowserStore(
BrowserState(
tabs = listOf(
@@ -214,7 +216,7 @@ class SessionSuggestionProviderTest {
}
@Test
- fun `Uses title for chip title when available, but falls back to URL`() = runBlocking {
+ fun `Uses title for chip title when available, but falls back to URL`() = runTest {
val store = BrowserStore(
BrowserState(
tabs = listOf(
diff --git a/components/feature/biometric-prompt/.gitignore b/components/feature/biometric-prompt/.gitignore
deleted file mode 100644
index 42afabfd2ab..00000000000
--- a/components/feature/biometric-prompt/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
\ No newline at end of file
diff --git a/components/feature/biometric-prompt/consumer-rules.pro b/components/feature/biometric-prompt/consumer-rules.pro
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/components/feature/biometric-prompt/src/main/java/mozilla/components/feature/biometric/AuthenticationCallbacks.kt b/components/feature/biometric-prompt/src/main/java/mozilla/components/feature/biometric/AuthenticationCallbacks.kt
deleted file mode 100644
index cf7d79e4542..00000000000
--- a/components/feature/biometric-prompt/src/main/java/mozilla/components/feature/biometric/AuthenticationCallbacks.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-/* 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.feature.biometric
-
-/**
- * Callbacks for BiometricPrompt Authentication
- */
-interface AuthenticationCallbacks {
- val onAuthFailure: () -> Unit
- val onAuthSuccess: () -> Unit
- val onAuthError: (errorText: String) -> Unit
-}
diff --git a/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/ContainerStorageTest.kt b/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/ContainerStorageTest.kt
index 605f97d35a0..45ea80c8728 100644
--- a/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/ContainerStorageTest.kt
+++ b/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/ContainerStorageTest.kt
@@ -11,7 +11,7 @@ import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.state.Container
import mozilla.components.browser.state.state.ContainerState.Color
import mozilla.components.browser.state.state.ContainerState.Icon
@@ -52,7 +52,7 @@ class ContainerStorageTest {
}
@Test
- fun testAddingContainer() = runBlockingTest {
+ fun testAddingContainer() = runTest {
storage.addContainer("1", "Personal", Color.RED, Icon.FINGERPRINT)
storage.addContainer("2", "Shopping", Color.BLUE, Icon.CART)
@@ -71,7 +71,7 @@ class ContainerStorageTest {
}
@Test
- fun testRemovingContainers() = runBlockingTest {
+ fun testRemovingContainers() = runTest {
storage.addContainer("1", "Personal", Color.RED, Icon.FINGERPRINT)
storage.addContainer("2", "Shopping", Color.BLUE, Icon.CART)
@@ -92,7 +92,7 @@ class ContainerStorageTest {
}
@Test
- fun testGettingContainers() = runBlockingTest {
+ fun testGettingContainers() = runTest {
storage.addContainer("1", "Personal", Color.RED, Icon.FINGERPRINT)
storage.addContainer("2", "Shopping", Color.BLUE, Icon.CART)
diff --git a/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/db/ContainerDaoTest.kt b/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/db/ContainerDaoTest.kt
index 8c0515253eb..5a42960298e 100644
--- a/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/db/ContainerDaoTest.kt
+++ b/components/feature/containers/src/androidTest/java/mozilla/components/feature/containers/db/ContainerDaoTest.kt
@@ -10,7 +10,7 @@ import androidx.paging.PagedList
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.state.ContainerState.Color
import mozilla.components.browser.state.state.ContainerState.Icon
import org.junit.After
@@ -48,7 +48,7 @@ class ContainerDaoTest {
}
@Test
- fun testAddingContainer() = runBlockingTest {
+ fun testAddingContainer() = runTest {
val container =
ContainerEntity(
contextId = UUID.randomUUID().toString(),
@@ -70,7 +70,7 @@ class ContainerDaoTest {
}
@Test
- fun testRemovingContainer() = runBlockingTest {
+ fun testRemovingContainer() = runTest {
val container1 =
ContainerEntity(
contextId = UUID.randomUUID().toString(),
diff --git a/components/feature/containers/src/test/java/mozilla/components/feature/containers/ContainerMiddlewareTest.kt b/components/feature/containers/src/test/java/mozilla/components/feature/containers/ContainerMiddlewareTest.kt
index 71d97afcc83..a3c1c5796cb 100644
--- a/components/feature/containers/src/test/java/mozilla/components/feature/containers/ContainerMiddlewareTest.kt
+++ b/components/feature/containers/src/test/java/mozilla/components/feature/containers/ContainerMiddlewareTest.kt
@@ -7,7 +7,6 @@ package mozilla.components.feature.containers
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.state.action.ContainerAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.ContainerState
@@ -16,8 +15,11 @@ import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import mozilla.components.support.test.whenever
import org.junit.Assert.assertEquals
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify
@@ -25,6 +27,9 @@ import org.mockito.Mockito.verify
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class ContainerMiddlewareTest {
+ @get:Rule
+ val mainCoroutineRule = MainCoroutineRule()
+
// Test container
private val container = ContainerState(
contextId = "contextId",
@@ -35,7 +40,7 @@ class ContainerMiddlewareTest {
@Test
fun `container storage stores the provided container on add container action`() =
- runBlockingTest {
+ runTestOnMain {
val storage = mockStorage()
val middleware = ContainerMiddleware(testContext, coroutineContext, containerStorage = storage)
val store = BrowserStore(
@@ -43,6 +48,9 @@ class ContainerMiddlewareTest {
middleware = listOf(middleware)
)
+ store.waitUntilIdle() // wait to consume InitAction
+ store.waitUntilIdle() // wait to consume AddContainersAction
+
store.dispatch(ContainerAction.AddContainerAction(container)).joinBlocking()
verify(storage).addContainer(
@@ -55,7 +63,7 @@ class ContainerMiddlewareTest {
@Test
fun `fetch the containers from the container storage and load into browser state on initialize container state action`() =
- runBlockingTest {
+ runTestOnMain {
val storage = mockStorage(listOf(container))
val middleware = ContainerMiddleware(testContext, coroutineContext, containerStorage = storage)
val store = BrowserStore(
@@ -63,8 +71,8 @@ class ContainerMiddlewareTest {
middleware = listOf(middleware)
)
- // Wait until init action is processed
- store.waitUntilIdle()
+ store.waitUntilIdle() // wait to consume InitAction
+ store.waitUntilIdle() // wait to consume AddContainersAction
verify(storage).getContainers()
assertEquals(container, store.state.containers["contextId"])
@@ -72,7 +80,7 @@ class ContainerMiddlewareTest {
@Test
fun `container storage removes the provided container on remove container action`() =
- runBlockingTest {
+ runTestOnMain {
val storage = mockStorage()
val middleware = ContainerMiddleware(testContext, coroutineContext, containerStorage = storage)
val store = BrowserStore(
@@ -84,6 +92,9 @@ class ContainerMiddlewareTest {
middleware = listOf(middleware)
)
+ store.waitUntilIdle() // wait to consume InitAction
+ store.waitUntilIdle() // wait to consume AddContainersAction
+
store.dispatch(ContainerAction.RemoveContainerAction(container.contextId))
.joinBlocking()
diff --git a/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuCandidate.kt b/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuCandidate.kt
index 3fd5ef1f9a5..24c4a1397b5 100644
--- a/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuCandidate.kt
+++ b/components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuCandidate.kt
@@ -92,19 +92,28 @@ data class ContextMenuCandidate(
/**
* Context Menu item: "Open Link in New Tab".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param tabsUseCases [TabsUseCases] used for adding new tabs.
+ * @param snackBarParentView The view in which to find a suitable parent for displaying the `Snackbar`.
+ * @param snackbarDelegate [SnackbarDelegate] used to actually show a `Snackbar`.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
*/
fun createOpenInNewTabCandidate(
context: Context,
tabsUseCases: TabsUseCases,
snackBarParentView: View,
- snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate()
+ snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(),
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
) = ContextMenuCandidate(
id = "mozac.feature.contextmenu.open_in_new_tab",
label = context.getString(R.string.mozac_feature_contextmenu_open_link_in_new_tab),
showFor = { tab, hitResult ->
tab.isUrlSchemeAllowed(hitResult.getLink()) &&
hitResult.isHttpLink() &&
- !tab.content.private
+ !tab.content.private &&
+ additionalValidation(tab, hitResult)
},
action = { parent, hitResult ->
val tab = tabsUseCases.addTab(
@@ -128,18 +137,26 @@ data class ContextMenuCandidate(
/**
* Context Menu item: "Open Link in Private Tab".
- */
+ *
+ * @param context [Context] used for various system interactions.
+ * @param tabsUseCases [TabsUseCases] used for adding new tabs.
+ * @param snackBarParentView The view in which to find a suitable parent for displaying the `Snackbar`.
+ * @param snackbarDelegate [SnackbarDelegate] used to actually show a `Snackbar`.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed. */
fun createOpenInPrivateTabCandidate(
context: Context,
tabsUseCases: TabsUseCases,
snackBarParentView: View,
- snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate()
+ snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(),
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
) = ContextMenuCandidate(
id = "mozac.feature.contextmenu.open_in_private_tab",
label = context.getString(R.string.mozac_feature_contextmenu_open_link_in_private_tab),
showFor = { tab, hitResult ->
tab.isUrlSchemeAllowed(hitResult.getLink()) &&
- hitResult.isHttpLink()
+ hitResult.isHttpLink() &&
+ additionalValidation(tab, hitResult)
},
action = { parent, hitResult ->
val tab = tabsUseCases.addTab(
@@ -163,16 +180,23 @@ data class ContextMenuCandidate(
/**
* Context Menu item: "Open Link in external App".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param appLinksUseCases [AppLinksUseCases] used to interact with urls that can be opened in 3rd party apps.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
*/
fun createOpenInExternalAppCandidate(
context: Context,
- appLinksUseCases: AppLinksUseCases
+ appLinksUseCases: AppLinksUseCases,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
) = ContextMenuCandidate(
id = "mozac.feature.contextmenu.open_in_external_app",
label = context.getString(R.string.mozac_feature_contextmenu_open_link_in_external_app),
showFor = { tab, hitResult ->
tab.isUrlSchemeAllowed(hitResult.getLink()) &&
- hitResult.canOpenInExternalApp(appLinksUseCases)
+ hitResult.canOpenInExternalApp(appLinksUseCases) &&
+ additionalValidation(tab, hitResult)
},
action = { _, hitResult ->
val link = hitResult.getLink()
@@ -189,47 +213,67 @@ data class ContextMenuCandidate(
/**
* Context Menu item: "Add to contact".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
*/
fun createAddContactCandidate(
- context: Context
+ context: Context,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
) = ContextMenuCandidate(
id = "mozac.feature.contextmenu.add_to_contact",
label = context.getString(R.string.mozac_feature_contextmenu_add_to_contact),
showFor = { tab, hitResult ->
tab.isUrlSchemeAllowed(hitResult.getLink()) &&
- hitResult.isMailto()
+ hitResult.isMailto() &&
+ additionalValidation(tab, hitResult)
},
action = { _, hitResult -> context.addContact(hitResult.getLink().stripMailToProtocol()) }
)
/**
* Context Menu item: "Share email address".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
*/
fun createShareEmailAddressCandidate(
- context: Context
+ context: Context,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
) = ContextMenuCandidate(
id = "mozac.feature.contextmenu.share_email",
label = context.getString(R.string.mozac_feature_contextmenu_share_email_address),
showFor = { tab, hitResult ->
tab.isUrlSchemeAllowed(hitResult.getLink()) &&
- hitResult.isMailto()
+ hitResult.isMailto() &&
+ additionalValidation(tab, hitResult)
},
action = { _, hitResult -> context.share(hitResult.getLink().stripMailToProtocol()) }
)
/**
* Context Menu item: "Copy email address".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param snackBarParentView The view in which to find a suitable parent for displaying the `Snackbar`.
+ * @param snackbarDelegate [SnackbarDelegate] used to actually show a `Snackbar`.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
*/
fun createCopyEmailAddressCandidate(
context: Context,
snackBarParentView: View,
- snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate()
+ snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(),
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
) = ContextMenuCandidate(
id = "mozac.feature.contextmenu.copy_email_address",
label = context.getString(R.string.mozac_feature_contextmenu_copy_email_address),
showFor = { tab, hitResult ->
tab.isUrlSchemeAllowed(hitResult.getLink()) &&
- hitResult.isMailto()
+ hitResult.isMailto() &&
+ additionalValidation(tab, hitResult)
},
action = { _, hitResult ->
val email = hitResult.getLink().stripMailToProtocol()
@@ -243,18 +287,27 @@ data class ContextMenuCandidate(
/**
* Context Menu item: "Open Image in New Tab".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param tabsUseCases [TabsUseCases] used for adding new tabs.
+ * @param snackBarParentView The view in which to find a suitable parent for displaying the `Snackbar`.
+ * @param snackbarDelegate [SnackbarDelegate] used to actually show a `Snackbar`.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
*/
fun createOpenImageInNewTabCandidate(
context: Context,
tabsUseCases: TabsUseCases,
snackBarParentView: View,
- snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate()
+ snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(),
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
) = ContextMenuCandidate(
id = "mozac.feature.contextmenu.open_image_in_new_tab",
label = context.getString(R.string.mozac_feature_contextmenu_open_image_in_new_tab),
showFor = { tab, hitResult ->
tab.isUrlSchemeAllowed(hitResult.getLink()) &&
- hitResult.isImage()
+ hitResult.isImage() &&
+ additionalValidation(tab, hitResult)
},
action = { parent, hitResult ->
val tab = tabsUseCases.addTab(
@@ -279,16 +332,23 @@ data class ContextMenuCandidate(
/**
* Context Menu item: "Save image".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param contextMenuUseCases [ContextMenuUseCases] used to integrate other features.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
*/
fun createSaveImageCandidate(
context: Context,
- contextMenuUseCases: ContextMenuUseCases
+ contextMenuUseCases: ContextMenuUseCases,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
) = ContextMenuCandidate(
id = "mozac.feature.contextmenu.save_image",
label = context.getString(R.string.mozac_feature_contextmenu_save_image),
showFor = { tab, hitResult ->
tab.isUrlSchemeAllowed(hitResult.getLink()) &&
- hitResult.isImage()
+ hitResult.isImage() &&
+ additionalValidation(tab, hitResult)
},
action = { tab, hitResult ->
contextMenuUseCases.injectDownload(
@@ -300,16 +360,23 @@ data class ContextMenuCandidate(
/**
* Context Menu item: "Save video".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param contextMenuUseCases [ContextMenuUseCases] used to integrate other features.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
*/
fun createSaveVideoAudioCandidate(
context: Context,
- contextMenuUseCases: ContextMenuUseCases
+ contextMenuUseCases: ContextMenuUseCases,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
) = ContextMenuCandidate(
id = "mozac.feature.contextmenu.save_video",
label = context.getString(R.string.mozac_feature_contextmenu_save_file_to_device),
showFor = { tab, hitResult ->
tab.isUrlSchemeAllowed(hitResult.getLink()) &&
- hitResult.isVideoAudio()
+ hitResult.isVideoAudio() &&
+ additionalValidation(tab, hitResult)
},
action = { tab, hitResult ->
contextMenuUseCases.injectDownload(
@@ -321,16 +388,23 @@ data class ContextMenuCandidate(
/**
* Context Menu item: "Save link".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param contextMenuUseCases [ContextMenuUseCases] used to integrate other features.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
*/
fun createDownloadLinkCandidate(
context: Context,
- contextMenuUseCases: ContextMenuUseCases
+ contextMenuUseCases: ContextMenuUseCases,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
) = ContextMenuCandidate(
id = "mozac.feature.contextmenu.download_link",
label = context.getString(R.string.mozac_feature_contextmenu_download_link),
showFor = { tab, hitResult ->
tab.isUrlSchemeAllowed(hitResult.getLink()) &&
- hitResult.isLinkForOtherThanWebpage()
+ hitResult.isLinkForOtherThanWebpage() &&
+ additionalValidation(tab, hitResult)
},
action = { tab, hitResult ->
contextMenuUseCases.injectDownload(
@@ -342,15 +416,21 @@ data class ContextMenuCandidate(
/**
* Context Menu item: "Share Link".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
*/
fun createShareLinkCandidate(
- context: Context
+ context: Context,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
) = ContextMenuCandidate(
id = "mozac.feature.contextmenu.share_link",
label = context.getString(R.string.mozac_feature_contextmenu_share_link),
showFor = { tab, hitResult ->
tab.isUrlSchemeAllowed(hitResult.getLink()) &&
- (hitResult.isUri() || hitResult.isImage() || hitResult.isVideoAudio())
+ (hitResult.isUri() || hitResult.isImage() || hitResult.isVideoAudio()) &&
+ additionalValidation(tab, hitResult)
},
action = { _, hitResult ->
val intent = Intent(Intent.ACTION_SEND).apply {
@@ -428,16 +508,23 @@ data class ContextMenuCandidate(
/**
* Context Menu item: "Share image"
+ *
+ * @param context [Context] used for various system interactions.
+ * @param contextMenuUseCases [ContextMenuUseCases] used to integrate other features.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
*/
fun createShareImageCandidate(
context: Context,
- contextMenuUseCases: ContextMenuUseCases
+ contextMenuUseCases: ContextMenuUseCases,
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
) = ContextMenuCandidate(
id = "mozac.feature.contextmenu.share_image",
label = context.getString(R.string.mozac_feature_contextmenu_share_image),
showFor = { tab, hitResult ->
tab.isUrlSchemeAllowed(hitResult.getLink()) &&
- hitResult.isImage()
+ hitResult.isImage() &&
+ additionalValidation(tab, hitResult)
},
action = { tab, hitResult ->
contextMenuUseCases.injectShareFromInternet(
@@ -452,17 +539,25 @@ data class ContextMenuCandidate(
/**
* Context Menu item: "Copy Link".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param snackBarParentView The view in which to find a suitable parent for displaying the `Snackbar`.
+ * @param snackbarDelegate [SnackbarDelegate] used to actually show a `Snackbar`.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
*/
fun createCopyLinkCandidate(
context: Context,
snackBarParentView: View,
- snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate()
+ snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(),
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
) = ContextMenuCandidate(
id = "mozac.feature.contextmenu.copy_link",
label = context.getString(R.string.mozac_feature_contextmenu_copy_link),
showFor = { tab, hitResult ->
tab.isUrlSchemeAllowed(hitResult.getLink()) &&
- (hitResult.isUri() || hitResult.isImage() || hitResult.isVideoAudio())
+ (hitResult.isUri() || hitResult.isImage() || hitResult.isVideoAudio()) &&
+ additionalValidation(tab, hitResult)
},
action = { _, hitResult ->
clipPlaintText(
@@ -475,17 +570,25 @@ data class ContextMenuCandidate(
/**
* Context Menu item: "Copy Image Location".
+ *
+ * @param context [Context] used for various system interactions.
+ * @param snackBarParentView The view in which to find a suitable parent for displaying the `Snackbar`.
+ * @param snackbarDelegate [SnackbarDelegate] used to actually show a `Snackbar`.
+ * @param additionalValidation Callback for the final validation in deciding whether this menu option
+ * will be shown. Will only be called if all the intrinsic validations passed.
*/
fun createCopyImageLocationCandidate(
context: Context,
snackBarParentView: View,
- snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate()
+ snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(),
+ additionalValidation: (SessionState, HitResult) -> Boolean = { _, _ -> true },
) = ContextMenuCandidate(
id = "mozac.feature.contextmenu.copy_image_location",
label = context.getString(R.string.mozac_feature_contextmenu_copy_image_location),
showFor = { tab, hitResult ->
tab.isUrlSchemeAllowed(hitResult.getLink()) &&
- hitResult.isImage()
+ hitResult.isImage() &&
+ additionalValidation(tab, hitResult)
},
action = { _, hitResult ->
clipPlaintText(
diff --git a/components/feature/contextmenu/src/main/res/values-ast/strings.xml b/components/feature/contextmenu/src/main/res/values-ast/strings.xml
index ebeed679f92..8626ca3c65b 100644
--- a/components/feature/contextmenu/src/main/res/values-ast/strings.xml
+++ b/components/feature/contextmenu/src/main/res/values-ast/strings.xml
@@ -33,7 +33,7 @@
Compartir la direición de corréu
- Copiar la direición de corréu
+ Copiar la direición de corréu electrónicuLa direición de corréu electrónicu copióse al cartafueyu
@@ -45,7 +45,7 @@
Compartir
- Unviar per corréu
+ Unviar per corréu electrónicuLlamar
diff --git a/components/feature/contextmenu/src/main/res/values-ban/strings.xml b/components/feature/contextmenu/src/main/res/values-ban/strings.xml
new file mode 100644
index 00000000000..a478cc786cf
--- /dev/null
+++ b/components/feature/contextmenu/src/main/res/values-ban/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ Unduh tautan
+
+ Ngbagiang tautan
+
+ Ngbagiang gambar
+
+ Raksa gambar
+
+ Raksa berkas ka piranti
+
+ Tambah ka kontak
+
+ Rereh
+
+ Bagiang
+
+ Rerepél
+
diff --git a/components/feature/contextmenu/src/main/res/values-es-rCL/strings.xml b/components/feature/contextmenu/src/main/res/values-es-rCL/strings.xml
index 9fa40bba7f4..da40bbb753a 100644
--- a/components/feature/contextmenu/src/main/res/values-es-rCL/strings.xml
+++ b/components/feature/contextmenu/src/main/res/values-es-rCL/strings.xml
@@ -7,7 +7,7 @@
Abrir imagen en una pestaña nueva
- Enlace de descarga
+ Descargar enlaceCompartir enlace
diff --git a/components/feature/contextmenu/src/main/res/values-is/strings.xml b/components/feature/contextmenu/src/main/res/values-is/strings.xml
index 4ca5116a1ec..cb04f5170de 100644
--- a/components/feature/contextmenu/src/main/res/values-is/strings.xml
+++ b/components/feature/contextmenu/src/main/res/values-is/strings.xml
@@ -3,7 +3,7 @@
Opna tengil í nýjum flipa
- Opna tengil í einkaflipa
+ Opna tengil í huliðsflipaOpna mynd í nýjum flipa
@@ -23,7 +23,7 @@
Nýr flipi opnaður
- Nýr einkaflipi opnaður
+ Nýr huliðsflipi opnaðurTengill afritaður á klippispjald
diff --git a/components/feature/contextmenu/src/main/res/values-skr/strings.xml b/components/feature/contextmenu/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..b6b9b164225
--- /dev/null
+++ b/components/feature/contextmenu/src/main/res/values-skr/strings.xml
@@ -0,0 +1,51 @@
+
+
+
+ نویں ٹیب وچ لنک کھولو
+
+ نجی ٹیب وچ لنک کھولو
+
+ نویں ٹیب وچ تصویر کھولو
+
+ لنک ڈاؤن لوڈ کرو
+
+ لنک شیئر کرو
+
+ تصویر شیئر کرو
+
+ لنک نقل کرو
+
+ تصویر مقام نقل کرو
+
+ تصویر محفوظ کرو
+
+ فائل ڈیوائس وچ محفوظ کرو
+
+ نواں ٹیب کھل ڳیا
+
+ نواں نجی ٹیب کھُل ڳیا
+
+ لنک کلپ بورڈ تے نقل تھی ڳیا
+
+ سوئچ
+
+ لنک ٻاہرلی ایپ وچ کھولو
+
+ ای میل پتہ شیئر کرو
+
+ ای میل پتہ نقل کرو
+
+ ای میل پتہ کلپ بورڈ تے نقل تھی ڳیا
+
+ رابطہ وچ شامل کرو
+
+ ڳولو
+
+ نجی ڳولݨ
+
+ شیئر
+
+ ای میل
+
+ فون کرو
+
diff --git a/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuCandidateTest.kt b/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuCandidateTest.kt
index 9b0d033afff..9ebe4ba0010 100644
--- a/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuCandidateTest.kt
+++ b/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuCandidateTest.kt
@@ -15,6 +15,7 @@ import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.EngineState
+import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.content.ShareInternetResourceState
import mozilla.components.browser.state.state.createTab
@@ -108,6 +109,30 @@ class ContextMenuCandidateTest {
)
}
+ @Test
+ fun `Candidate 'Open Link in New Tab' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val openInNewTab = ContextMenuCandidate.createOpenInNewTabCandidate(
+ testContext, mock(), mock(), snackbarDelegate, additionalValidation
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ openInNewTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ openInNewTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org")
+ )
+ )
+ }
+
@Test
fun `Candidate 'Open Link in New Tab' action properly executes for session with a contextId`() {
val store = BrowserStore(
@@ -280,6 +305,37 @@ class ContextMenuCandidateTest {
)
}
+ @Test
+ fun `Candidate 'Open Link in Private Tab' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val openInPrivateTab = ContextMenuCandidate.createOpenInPrivateTabCandidate(
+ testContext, mock(), mock(), snackbarDelegate, additionalValidation
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ openInPrivateTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ openInPrivateTab.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ openInPrivateTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org")
+ )
+ )
+ }
+
@Test
fun `Candidate 'Open Link in Private Tab' action properly executes and shows snackbar`() {
val store = BrowserStore(
@@ -458,6 +514,30 @@ class ContextMenuCandidateTest {
assertEquals("https://firefox.com", store.state.selectedTab!!.content.url)
}
+ @Test
+ fun `Candidate 'Open Image in New Tab' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val openImageInTab = ContextMenuCandidate.createOpenImageInNewTabCandidate(
+ testContext, mock(), mock(), snackbarDelegate, additionalValidation
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ openImageInTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ openImageInTab.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org")
+ )
+ )
+ }
+
@Test
fun `Candidate 'Open Image in New Tab' opens in private tab if session is private`() {
val store = BrowserStore(
@@ -601,6 +681,30 @@ class ContextMenuCandidateTest {
)
}
+ @Test
+ fun `Candidate 'Save image' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val saveImage = ContextMenuCandidate.createSaveImageCandidate(
+ testContext, mock(), additionalValidation
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ saveImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ saveImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org")
+ )
+ )
+ }
+
@Test
fun `Candidate 'Save video and audio'`() {
val store = BrowserStore(
@@ -686,6 +790,30 @@ class ContextMenuCandidateTest {
)
}
+ @Test
+ fun `Candidate 'Save video and audio' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val saveVideoAudio = ContextMenuCandidate.createSaveVideoAudioCandidate(
+ testContext, mock(), additionalValidation
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ saveVideoAudio.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.VIDEO("https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ saveVideoAudio.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.AUDIO("https://www.mozilla.org")
+ )
+ )
+ }
+
@Test
fun `Candidate 'download link'`() {
val store = BrowserStore(
@@ -800,6 +928,37 @@ class ContextMenuCandidateTest {
assertTrue(store.state.tabs.first().content.download!!.private)
}
+ @Test
+ fun `Candidate 'download link' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val downloadLink = ContextMenuCandidate.createDownloadLinkCandidate(
+ testContext, mock(), additionalValidation
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ downloadLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org")
+ )
+ )
+ }
+
@Test
fun `Get link for image, video, audio gets title if title is set`() {
val titleString = "test title"
@@ -943,6 +1102,58 @@ class ContextMenuCandidateTest {
verify(context).startActivity(any())
}
+ @Test
+ fun `Candidate 'Share Link' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val shareLink = ContextMenuCandidate.createShareLinkCandidate(
+ testContext, additionalValidation
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ shareLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ shareLink.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ shareLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ shareLink.showFor(
+ createTab("test://www.mozilla.org"),
+ HitResult.UNKNOWN("test://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ shareLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ shareLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.VIDEO("https://www.mozilla.org")
+ )
+ )
+ }
+
@Test
fun `Candidate 'Share image'`() {
val store = BrowserStore(
@@ -992,6 +1203,30 @@ class ContextMenuCandidateTest {
assertEquals(store.state.tabs.first().content.private, shareStateCaptor.value.private)
}
+ @Test
+ fun `Candidate 'Share image' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val shareImage = ContextMenuCandidate.createShareImageCandidate(
+ testContext, mock(), additionalValidation
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ shareImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ shareImage.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org")
+ )
+ )
+ }
+
@Test
fun `Candidate 'Copy Link'`() {
val parentView = CoordinatorLayout(testContext)
@@ -1069,6 +1304,58 @@ class ContextMenuCandidateTest {
)
}
+ @Test
+ fun `Candidate 'Copy Link' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val copyLink = ContextMenuCandidate.createCopyLinkCandidate(
+ testContext, mock(), snackbarDelegate, additionalValidation
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ copyLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ copyLink.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ copyLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ copyLink.showFor(
+ createTab("test://www.mozilla.org"),
+ HitResult.UNKNOWN("test://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ copyLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ copyLink.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.VIDEO("https://www.mozilla.org")
+ )
+ )
+ }
+
@Test
fun `Candidate 'Copy Image Location'`() {
val parentView = CoordinatorLayout(testContext)
@@ -1139,6 +1426,30 @@ class ContextMenuCandidateTest {
)
}
+ @Test
+ fun `Candidate 'Copy Image Location' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val copyImageLocation = ContextMenuCandidate.createCopyImageLocationCandidate(
+ testContext, mock(), snackbarDelegate, additionalValidation
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ copyImageLocation.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE_SRC("https://www.mozilla.org", "https://www.mozilla.org")
+ )
+ )
+
+ assertFalse(
+ copyImageLocation.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.IMAGE("https://www.mozilla.org")
+ )
+ )
+ }
+
@Test
fun `Candidate 'Open in external app'`() {
val tab = createTab("https://www.mozilla.org")
@@ -1238,6 +1549,56 @@ class ContextMenuCandidateTest {
verify(openAppLinkRedirectMock, times(2)).invoke(any(), anyBoolean(), any())
}
+ @Test
+ fun `Candidate 'Open in external app' allows for an additional validation for it to be shown`() {
+ val tab = createTab("https://www.mozilla.org")
+ val getAppLinkRedirectMock: AppLinksUseCases.GetAppLinkRedirect = mock()
+ doReturn(
+ AppLinkRedirect(mock(), null, null)
+ ).`when`(getAppLinkRedirectMock).invoke(eq("https://www.example.com"))
+ doReturn(
+ AppLinkRedirect(null, null, mock())
+ ).`when`(getAppLinkRedirectMock).invoke(eq("intent:www.example.com#Intent;scheme=https;package=org.mozilla.fenix;end"))
+ val openAppLinkRedirectMock: AppLinksUseCases.OpenAppLinkRedirect = mock()
+ val appLinksUseCasesMock: AppLinksUseCases = mock()
+ doReturn(getAppLinkRedirectMock).`when`(appLinksUseCasesMock).appLinkRedirectIncludeInstall
+ doReturn(openAppLinkRedirectMock).`when`(appLinksUseCasesMock).openAppLink
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val openLinkInExternalApp = ContextMenuCandidate.createOpenInExternalAppCandidate(
+ testContext, appLinksUseCasesMock, additionalValidation
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ openLinkInExternalApp.showFor(
+ tab,
+ HitResult.UNKNOWN("https://www.example.com")
+ )
+ )
+
+ assertFalse(
+ openLinkInExternalApp.showFor(
+ tab,
+ HitResult.UNKNOWN("intent:www.example.com#Intent;scheme=https;package=org.mozilla.fenix;end")
+ )
+ )
+
+ assertFalse(
+ openLinkInExternalApp.showFor(
+ tab,
+ HitResult.VIDEO("https://www.example.com")
+ )
+ )
+
+ assertFalse(
+ openLinkInExternalApp.showFor(
+ tab,
+ HitResult.AUDIO("https://www.example.com")
+ )
+ )
+ }
+
@Test
fun `Candidate 'Copy email address'`() {
val parentView = CoordinatorLayout(testContext)
@@ -1301,6 +1662,30 @@ class ContextMenuCandidateTest {
)
}
+ @Test
+ fun `Candidate 'Copy email address' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val copyEmailAddress = ContextMenuCandidate.createCopyEmailAddressCandidate(
+ testContext, mock(), snackbarDelegate, additionalValidation
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ copyEmailAddress.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("mailto:example@example.com")
+ )
+ )
+
+ assertFalse(
+ copyEmailAddress.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("mailto:example.com")
+ )
+ )
+ }
+
@Test
fun `Candidate 'Share email address'`() {
val context = spy(testContext)
@@ -1356,6 +1741,30 @@ class ContextMenuCandidateTest {
verify(context).startActivity(any())
}
+ @Test
+ fun `Candidate 'Share email address' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val shareEmailAddress = ContextMenuCandidate.createShareEmailAddressCandidate(
+ testContext, additionalValidation
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ shareEmailAddress.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("mailto:example@example.com")
+ )
+ )
+
+ assertFalse(
+ shareEmailAddress.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("mailto:example.com")
+ )
+ )
+ }
+
@Test
fun `Candidate 'Add to contacts'`() {
val context = spy(testContext)
@@ -1411,6 +1820,30 @@ class ContextMenuCandidateTest {
verify(context).startActivity(any())
}
+ @Test
+ fun `Candidate 'Add to contacts' allows for an additional validation for it to be shown`() {
+ val additionalValidation = { _: SessionState, _: HitResult -> false }
+ val addToContacts = ContextMenuCandidate.createAddContactCandidate(
+ testContext, additionalValidation
+ )
+
+ // By default in the below cases the candidate will be shown. 'additionalValidation' changes that.
+
+ assertFalse(
+ addToContacts.showFor(
+ createTab("https://www.mozilla.org"),
+ HitResult.UNKNOWN("mailto:example@example.com")
+ )
+ )
+
+ assertFalse(
+ addToContacts.showFor(
+ createTab("https://www.mozilla.org", private = true),
+ HitResult.UNKNOWN("mailto:example.com")
+ )
+ )
+ }
+
@Test
fun `GIVEN SessionState with null EngineSession WHEN isUrlSchemeAllowed is called THEN it returns true`() {
val sessionState = TabSessionState(
diff --git a/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuFeatureTest.kt b/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuFeatureTest.kt
index 3c64bebf6d1..05511500a84 100644
--- a/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuFeatureTest.kt
+++ b/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuFeatureTest.kt
@@ -9,10 +9,6 @@ import android.view.View
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.selector.findTab
@@ -29,13 +25,14 @@ import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.`when`
@@ -45,14 +42,14 @@ import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class ContextMenuFeatureTest {
- private val testDispatcher = TestCoroutineDispatcher()
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
private lateinit var store: BrowserStore
@Before
fun setUp() {
- Dispatchers.setMain(testDispatcher)
-
store = BrowserStore(
BrowserState(
tabs = listOf(
@@ -63,12 +60,6 @@ class ContextMenuFeatureTest {
)
}
- @After
- fun tearDown() {
- Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
- }
-
@Test
fun `New HitResult for selected session will cause fragment transaction`() {
val fragmentManager = mockFragmentManager()
@@ -92,7 +83,7 @@ class ContextMenuFeatureTest {
)
).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(fragmentManager).beginTransaction()
verify(view).performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
@@ -122,7 +113,7 @@ class ContextMenuFeatureTest {
)
).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(fragmentManager, never()).beginTransaction()
verify(view, never()).performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
@@ -155,7 +146,7 @@ class ContextMenuFeatureTest {
feature.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(fragment).feature = feature
verify(view, never()).performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
@@ -185,7 +176,7 @@ class ContextMenuFeatureTest {
feature.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(fragmentManager).beginTransaction()
verify(transaction).remove(fragment)
@@ -219,7 +210,7 @@ class ContextMenuFeatureTest {
feature.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(fragmentManager).beginTransaction()
verify(transaction).remove(fragment)
@@ -289,7 +280,7 @@ class ContextMenuFeatureTest {
feature.onMenuCancelled("test-tab")
store.waitUntilIdle()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(store.state.findTab("test-tab")!!.content.hitResult)
}
@@ -330,7 +321,7 @@ class ContextMenuFeatureTest {
)
store.waitUntilIdle()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNotNull(store.state.findTab("test-tab")!!.content.hitResult)
assertFalse(actionInvoked)
@@ -338,7 +329,7 @@ class ContextMenuFeatureTest {
feature.onMenuItemSelected("test-tab", "test-id")
store.waitUntilIdle()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNull(store.state.findTab("test-tab")!!.content.hitResult)
assertTrue(actionInvoked)
diff --git a/components/feature/customtabs/src/main/res/values-skr/strings.xml b/components/feature/customtabs/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..1336257ab3f
--- /dev/null
+++ b/components/feature/customtabs/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+
+
+ پچھلی ایپ تے واپس ون٘ڄو
+ لنک شیئر کرو
+
diff --git a/components/feature/customtabs/src/main/res/values-ug/strings.xml b/components/feature/customtabs/src/main/res/values-ug/strings.xml
new file mode 100644
index 00000000000..6091e4f4e4a
--- /dev/null
+++ b/components/feature/customtabs/src/main/res/values-ug/strings.xml
@@ -0,0 +1,5 @@
+
+
+ ئالدىنقى ئەپكە قايت
+ ئۇلانمىنى ئورتاقلاش
+
diff --git a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/AbstractCustomTabsServiceTest.kt b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/AbstractCustomTabsServiceTest.kt
index 3651f066055..c49dc23fc9a 100644
--- a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/AbstractCustomTabsServiceTest.kt
+++ b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/AbstractCustomTabsServiceTest.kt
@@ -12,22 +12,17 @@ import android.support.customtabs.ICustomTabsCallback
import android.support.customtabs.ICustomTabsService
import androidx.browser.customtabs.CustomTabsService
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
import mozilla.components.concept.engine.Engine
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
import mozilla.components.service.digitalassetlinks.RelationChecker
import mozilla.components.support.test.mock
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
-import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.doReturn
@@ -36,21 +31,8 @@ import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class AbstractCustomTabsServiceTest {
- @ExperimentalCoroutinesApi
- private val testDispatcher = TestCoroutineDispatcher()
-
- @ExperimentalCoroutinesApi
- @Before
- fun setUp() {
- Dispatchers.setMain(testDispatcher)
- }
-
- @ExperimentalCoroutinesApi
- @After
- fun tearDown() {
- Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
- }
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
@Test
fun customTabService() {
diff --git a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeatureTest.kt b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeatureTest.kt
index 3ad7db2d096..e2a1042c961 100644
--- a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeatureTest.kt
+++ b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabsToolbarFeatureTest.kt
@@ -14,7 +14,7 @@ import android.widget.ImageButton
import androidx.core.view.forEach
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import mozilla.components.browser.menu.BrowserMenuBuilder
@@ -857,7 +857,7 @@ class CustomTabsToolbarFeatureTest {
@Test
fun `show title only if not empty`() {
- val dispatcher = TestCoroutineDispatcher()
+ val dispatcher = UnconfinedTestDispatcher()
Dispatchers.setMain(dispatcher)
val tab = createCustomTab(
@@ -898,8 +898,6 @@ class CustomTabsToolbarFeatureTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
-
assertEquals("Internet for people, not profit - Mozilla", toolbar.title)
Dispatchers.resetMain()
@@ -907,7 +905,7 @@ class CustomTabsToolbarFeatureTest {
@Test
fun `Will use URL as title if title was shown once and is now empty`() {
- val dispatcher = TestCoroutineDispatcher()
+ val dispatcher = UnconfinedTestDispatcher()
Dispatchers.setMain(dispatcher)
val tab = createCustomTab(
@@ -947,64 +945,48 @@ class CustomTabsToolbarFeatureTest {
ContentAction.UpdateUrlAction("mozilla", "https://www.mozilla.org/en-US/firefox/")
).joinBlocking()
- dispatcher.advanceUntilIdle()
-
assertEquals("", toolbar.title)
store.dispatch(
ContentAction.UpdateTitleAction("mozilla", "Firefox - Protect your life online with privacy-first products")
).joinBlocking()
- dispatcher.advanceUntilIdle()
-
assertEquals("Firefox - Protect your life online with privacy-first products", toolbar.title)
store.dispatch(
ContentAction.UpdateUrlAction("mozilla", "https://github.com/mozilla-mobile/android-components")
).joinBlocking()
- dispatcher.advanceUntilIdle()
-
assertEquals("https://github.com/mozilla-mobile/android-components", toolbar.title)
store.dispatch(
ContentAction.UpdateTitleAction("mozilla", "Le GitHub")
).joinBlocking()
- dispatcher.advanceUntilIdle()
-
assertEquals("Le GitHub", toolbar.title)
store.dispatch(
ContentAction.UpdateUrlAction("mozilla", "https://github.com/mozilla-mobile/fenix")
).joinBlocking()
- dispatcher.advanceUntilIdle()
-
assertEquals("https://github.com/mozilla-mobile/fenix", toolbar.title)
store.dispatch(
ContentAction.UpdateTitleAction("mozilla", "")
).joinBlocking()
- dispatcher.advanceUntilIdle()
-
assertEquals("https://github.com/mozilla-mobile/fenix", toolbar.title)
store.dispatch(
ContentAction.UpdateTitleAction("mozilla", "A collection of Android libraries to build browsers or browser-like applications.")
).joinBlocking()
- dispatcher.advanceUntilIdle()
-
assertEquals("A collection of Android libraries to build browsers or browser-like applications.", toolbar.title)
store.dispatch(
ContentAction.UpdateTitleAction("mozilla", "")
).joinBlocking()
- dispatcher.advanceUntilIdle()
-
assertEquals("https://github.com/mozilla-mobile/fenix", toolbar.title)
}
diff --git a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserverTest.kt b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserverTest.kt
index 9f8a7c2ab35..c5eb522a37c 100644
--- a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserverTest.kt
+++ b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserverTest.kt
@@ -90,6 +90,7 @@ class CustomTabSessionTitleObserverTest {
override fun removeNavigationAction(action: Toolbar.Action) = Unit
override fun addEditActionStart(action: Toolbar.Action) = Unit
override fun addEditActionEnd(action: Toolbar.Action) = Unit
+ override fun removeEditActionEnd(action: Toolbar.Action) = Unit
override fun setOnEditListener(listener: Toolbar.OnEditListener) = Unit
override fun displayMode() = Unit
override fun editMode() = Unit
diff --git a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeatureTest.kt b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeatureTest.kt
index 9f359e41125..87c26780af9 100644
--- a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeatureTest.kt
+++ b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/OriginVerifierFeatureTest.kt
@@ -10,7 +10,7 @@ import androidx.browser.customtabs.CustomTabsSessionToken
import androidx.core.net.toUri
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.feature.customtabs.store.CustomTabState
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
import mozilla.components.feature.customtabs.store.OriginRelationPair
@@ -37,14 +37,14 @@ import org.mockito.Mockito.verify
class OriginVerifierFeatureTest {
@Test
- fun `verify fails if no creatorPackageName is saved`() = runBlockingTest {
+ fun `verify fails if no creatorPackageName is saved`() = runTest {
val feature = OriginVerifierFeature(mock(), mock(), mock())
assertFalse(feature.verify(CustomTabState(), mock(), RELATION_HANDLE_ALL_URLS, mock()))
}
@Test
- fun `verify returns existing relationship`() = runBlockingTest {
+ fun `verify returns existing relationship`() = runTest {
val feature = OriginVerifierFeature(mock(), mock(), mock())
val origin = "https://example.com".toUri()
val state = CustomTabState(
@@ -61,7 +61,7 @@ class OriginVerifierFeatureTest {
}
@Test
- fun `verify checks new relationships`() = runBlockingTest {
+ fun `verify checks new relationships`() = runTest {
val store: CustomTabsServiceStore = mock()
val verifier: OriginVerifier = mock()
val feature = spy(OriginVerifierFeature(mock(), mock()) { store.dispatch(it) })
diff --git a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/verify/OriginVerifierTest.kt b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/verify/OriginVerifierTest.kt
index e0a604deb37..ca6dd8055b7 100644
--- a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/verify/OriginVerifierTest.kt
+++ b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/verify/OriginVerifierTest.kt
@@ -10,7 +10,7 @@ import androidx.browser.customtabs.CustomTabsService.RELATION_USE_AS_ORIGIN
import androidx.core.net.toUri
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.fetch.Response
import mozilla.components.service.digitalassetlinks.AssetDescriptor
import mozilla.components.service.digitalassetlinks.Relation
@@ -49,14 +49,14 @@ class OriginVerifierTest {
}
@Test
- fun `only HTTPS allowed`() = runBlocking {
+ fun `only HTTPS allowed`() = runTest {
val verifier = buildVerifier(RELATION_HANDLE_ALL_URLS)
assertFalse(verifier.verifyOrigin("LOL".toUri()))
assertFalse(verifier.verifyOrigin("http://www.android.com".toUri()))
}
@Test
- fun verifyOrigin() = runBlocking {
+ fun verifyOrigin() = runTest {
val verifier = buildVerifier(RELATION_USE_AS_ORIGIN)
doReturn(true).`when`(checker).checkRelationship(
AssetDescriptor.Web("https://www.example.com"),
diff --git a/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/OnDeviceDownloadStorageTest.kt b/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/OnDeviceDownloadStorageTest.kt
index f4c984c8680..d1c858068df 100644
--- a/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/OnDeviceDownloadStorageTest.kt
+++ b/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/OnDeviceDownloadStorageTest.kt
@@ -13,8 +13,7 @@ import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.feature.downloads.db.DownloadsDatabase
import mozilla.components.feature.downloads.db.Migrations
@@ -165,7 +164,7 @@ class OnDeviceDownloadStorageTest {
}
@Test
- fun testAddingDownload() = runBlockingTest {
+ fun testAddingDownload() = runTest {
val download1 = createMockDownload("1", "url1")
val download2 = createMockDownload("2", "url2")
val download3 = createMockDownload("3", "url3")
@@ -184,7 +183,7 @@ class OnDeviceDownloadStorageTest {
}
@Test
- fun testAddingDataURLDownload() = runBlockingTest {
+ fun testAddingDataURLDownload() = runTest {
val download1 = createMockDownload("1", "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==")
val download2 = createMockDownload("2", "url2")
@@ -200,7 +199,7 @@ class OnDeviceDownloadStorageTest {
}
@Test
- fun testUpdatingDataURLDownload() = runBlockingTest {
+ fun testUpdatingDataURLDownload() = runTest {
val download1 = createMockDownload("1", "url1")
val download2 = createMockDownload("2", "url2")
@@ -227,7 +226,7 @@ class OnDeviceDownloadStorageTest {
}
@Test
- fun testRemovingDownload() = runBlockingTest {
+ fun testRemovingDownload() = runTest {
val download1 = createMockDownload("1", "url1")
val download2 = createMockDownload("2", "url2")
@@ -246,7 +245,7 @@ class OnDeviceDownloadStorageTest {
}
@Test
- fun testGettingDownloads() = runBlockingTest {
+ fun testGettingDownloads() = runTest {
val download1 = createMockDownload("1", "url1")
val download2 = createMockDownload("2", "url2")
@@ -262,7 +261,7 @@ class OnDeviceDownloadStorageTest {
}
@Test
- fun testRemovingDownloads() = runBlocking {
+ fun testRemovingDownloads() = runTest {
for (index in 1..2) {
storage.add(createMockDownload(index.toString(), "url1"))
}
diff --git a/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/db/DownloadDaoTest.kt b/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/db/DownloadDaoTest.kt
index 04acf453d47..72cfae55662 100644
--- a/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/db/DownloadDaoTest.kt
+++ b/components/feature/downloads/src/androidTest/java/mozilla/components/feature/downloads/db/DownloadDaoTest.kt
@@ -9,7 +9,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.paging.PagedList
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.feature.downloads.DownloadStorage
import org.junit.After
@@ -46,7 +46,7 @@ class DownloadDaoTest {
}
@Test
- fun testInsertingAndReadingDownloads() = runBlocking {
+ fun testInsertingAndReadingDownloads() = runTest {
val download = insertMockDownload("1", "https://www.mozilla.org/file1.txt")
val pagedList = getDownloadsPagedList()
@@ -55,7 +55,7 @@ class DownloadDaoTest {
}
@Test
- fun testRemoveAllDownloads() = runBlocking {
+ fun testRemoveAllDownloads() = runTest {
for (index in 1..4) {
insertMockDownload(index.toString(), "https://www.mozilla.org/file1.txt")
}
@@ -71,7 +71,7 @@ class DownloadDaoTest {
}
@Test
- fun testRemovingDownloads() = runBlocking {
+ fun testRemovingDownloads() = runTest {
for (index in 1..2) {
insertMockDownload(index.toString(), "https://www.mozilla.org/file1.txt")
}
@@ -90,7 +90,7 @@ class DownloadDaoTest {
}
@Test
- fun testUpdateDownload() = runBlocking {
+ fun testUpdateDownload() = runTest {
insertMockDownload("1", "https://www.mozilla.org/file1.txt")
var pagedList = getDownloadsPagedList()
diff --git a/components/feature/downloads/src/main/res/values-ast/strings.xml b/components/feature/downloads/src/main/res/values-ast/strings.xml
index bade1812a80..ae218de9c46 100644
--- a/components/feature/downloads/src/main/res/values-ast/strings.xml
+++ b/components/feature/downloads/src/main/res/values-ast/strings.xml
@@ -4,11 +4,11 @@
Descargues
- Posó la descarga
+ Descarga en posaCompletóse la descarga
- Falló la descarga
+ La descarga fallóBaxar (%1$s)
@@ -20,10 +20,10 @@
%1$s nun pue baxar esti tipu de ficheru
- Nun pudo abrise\'l ficheru
+ Nun se pudo abrir el ficheru
- Nun s\'atopó nenguna aplicación p\'abrir ficheros %1$s
+ Nun s\'atopó nenguna aplicación p\'abrir ficheros «%1$s»Posar
@@ -39,7 +39,16 @@
Zarrar
- Completar l\'aición col usu de:
+ Completar l\'aición con:
-->
- Nun ye posible abrir %1$s
+ Nun ye posible abrir «%1$s»
+
+
+ ¿Quies encaboxar les descargues privaes?
+
+ Si zarres agora toles llingüetes privaes, va encaboxase la descarga de «%1$s». ¿De xuru que quies colar del mou de restolar en privao?
+
+ Encaboxales
+
+ Quedar
diff --git a/components/feature/downloads/src/main/res/values-bg/strings.xml b/components/feature/downloads/src/main/res/values-bg/strings.xml
index 31865c70e6a..77d607e004d 100644
--- a/components/feature/downloads/src/main/res/values-bg/strings.xml
+++ b/components/feature/downloads/src/main/res/values-bg/strings.xml
@@ -44,4 +44,11 @@
Не успя да се отвори %1$sЗа сваляне на файлове е необходимо за достъп до медия и файлове. За да разрешите, влезте в настройки на Android, след което докоснете разрешения и позволяване.
+
+
+ Ако затворите всички поверителни раздели, %1$s изтегляния ще бъдат прекъснати. Сигурни ли сте, че искате да напуснете поверителното разглеждане?
+
+ Прекъсване
+
+ Оставане в поверително разглеждане
diff --git a/components/feature/downloads/src/main/res/values-eo/strings.xml b/components/feature/downloads/src/main/res/values-eo/strings.xml
index 29ca049463f..dc042665017 100644
--- a/components/feature/downloads/src/main/res/values-eo/strings.xml
+++ b/components/feature/downloads/src/main/res/values-eo/strings.xml
@@ -47,6 +47,12 @@
La permeso aliri la konservejon de dosieroj kaj aŭdvidaĵojn estas bezonata por elŝuti dosierojn. Iru al la agordoj de Android, tuŝetu Permesoj kaj poste Permesi.
+
+ Ĉu nuligi privatajn elŝutojn?
+
+ Se vi fermas ĉiujn viajn langetojn de privata retumo nun, %1$s elŝutoj estos nuligitaj. Ĉu vi certe volas forlasi la privatan retumon?Nuligi elŝutojn
-
+
+ Resti en privata retumo
+
diff --git a/components/feature/downloads/src/main/res/values-gd/strings.xml b/components/feature/downloads/src/main/res/values-gd/strings.xml
index 66f4173ac30..e38a01f7e1a 100644
--- a/components/feature/downloads/src/main/res/values-gd/strings.xml
+++ b/components/feature/downloads/src/main/res/values-gd/strings.xml
@@ -44,4 +44,13 @@
Cha ghabh %1$s fhosgladhTha feum air cead inntrigidh do dh’fhaidhlichean is meadhanan mus luchdaich thu a-nuas faidhle. Tadhail air roghainnean Android is thoir gnogag air a’ chead.
+
+
+ A bheil thu airson sgur de gach luchdadh a-nuas prìobhaideach?
+
+ Ma dhùineas tu gach taba brabhsaidh prìobhaideach an-dràsta, thèid crìoch a chur air luchdadh a-nuas an fhaidhle “%1$s”. A bheil thu cinnteach gu bheil thu airson am brabhsadh prìobhaideach fhàgail?
+
+ Sguir de gach luchdadh a-nuas
+
+ Fuirich sa bhrabhsadh phrìobhaideach
diff --git a/components/feature/downloads/src/main/res/values-kab/strings.xml b/components/feature/downloads/src/main/res/values-kab/strings.xml
index 00fc60d1e87..6d7c2867521 100644
--- a/components/feature/downloads/src/main/res/values-kab/strings.xml
+++ b/components/feature/downloads/src/main/res/values-kab/strings.xml
@@ -6,14 +6,14 @@
Asader yesteɛfa
- Asider yemmed
+ Asader yemmedIfuyla ittwazedmen
- Asider (%1$s)
+ Asader (%1$s)
- Sider
+ SaderSefsex
diff --git a/components/feature/downloads/src/main/res/values-skr/strings.xml b/components/feature/downloads/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..ff8515d020b
--- /dev/null
+++ b/components/feature/downloads/src/main/res/values-skr/strings.xml
@@ -0,0 +1,60 @@
+
+
+
+ ڈاؤن لوڈاں
+
+
+ ڈاؤن لوڈ رک ڳیا
+
+ ڈاؤن لوڈ مکمل تھی ڳیا
+
+ ڈاؤن لوڈ ناکام تھیا
+
+
+ ڈاؤن لوڈ(%1$s)
+
+ ڈاؤن لوڈ
+
+ منسوخ
+
+
+ %1$s ایہ فائل قسم ڈاؤن لوڈ کائنی کر سڳدا
+
+
+ فائل کائنی کھول سڳا
+
+
+ %1$s فائلاں کھولݨ کیتے کوئی ایپ کائنی لبھی
+
+
+ ذرا روکو
+
+ ولدا جاری کرو
+
+ منسوخ
+
+
+ کھولو
+
+ ولدا کوشش کرو
+
+
+ بند کرو
+
+
+ ایں کوں ورتݨ نال عمل پورا کرو
+ -->
+ %1$s کھولݨ وچ ناکام ریہا
+
+
+ فائلاں کوں ڈاؤن لوڈ کرݨ کیتے فائلاں تے میڈیا دی اجازت تائیں رسائی دی لوڑ ہے۔ اینڈرائیڈ ترتیباں تے ون٘ڄو، اجازتاں تے انگل پھیرو تے اجازت ݙیوو تے انگل پھیرو۔
+
+
+ نجی ڈاؤن لوڈاں منسوخ کروں؟
+
+ جے تساں ہݨ ساریاں نجی ٹیباں بند کریندے ہو، %1$s ڈوان لوڈ منسوخ کر ݙتی ویسی۔ بھل ا تہاکوں پک ہے جو تساں نجی براؤزنگ چھوڑݨ چاہندے ہو؟
+
+ ڈاؤن لوڈ منسوخ کرو
+
+ نجی براؤزنگ وچ راہوو
+
diff --git a/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/AbstractFetchDownloadServiceTest.kt b/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/AbstractFetchDownloadServiceTest.kt
index 14962b27365..68aece8291c 100644
--- a/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/AbstractFetchDownloadServiceTest.kt
+++ b/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/AbstractFetchDownloadServiceTest.kt
@@ -22,15 +22,13 @@ import androidx.core.net.toUri
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.runBlockingTest
-import kotlinx.coroutines.test.setMain
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.action.DownloadAction
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.state.content.DownloadState.Status.COMPLETED
@@ -61,7 +59,7 @@ import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
@@ -99,7 +97,6 @@ import org.robolectric.shadows.ShadowNotificationManager
import java.io.File
import java.io.IOException
import java.io.InputStream
-import java.lang.IllegalArgumentException
import kotlin.random.Random
@RunWith(AndroidJUnit4::class)
@@ -108,17 +105,24 @@ class AbstractFetchDownloadServiceTest {
@Rule @JvmField
val folder = TemporaryFolder()
+ // We need different scopes and schedulers because:
+ // - the service will continuously try to update the download notification using MainScope()
+ // - if using the same scope in tests the test won't end
+ // - need a way to advance main dispatcher used by the service.
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val mainDispatcher = coroutinesTestRule.testDispatcher
+ private val testsDispatcher = UnconfinedTestDispatcher(TestCoroutineScheduler())
+
@Mock private lateinit var client: Client
private lateinit var browserStore: BrowserStore
@Mock private lateinit var broadcastManager: LocalBroadcastManager
private lateinit var service: AbstractFetchDownloadService
- private val testDispatcher = TestCoroutineDispatcher()
private lateinit var shadowNotificationService: ShadowNotificationManager
@Before
fun setup() {
- Dispatchers.setMain(testDispatcher)
openMocks(this)
browserStore = BrowserStore()
service = spy(object : AbstractFetchDownloadService() {
@@ -134,13 +138,8 @@ class AbstractFetchDownloadServiceTest {
shadowOf(testContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
}
- @After
- fun afterEach() {
- Dispatchers.resetMain()
- }
-
@Test
- fun `begins download when started`() = runBlocking {
+ fun `begins download when started`() = runTest(testsDispatcher) {
val download = DownloadState("https://example.com/file.txt", "file.txt")
val response = Response(
"https://example.com/file.txt",
@@ -169,7 +168,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `WHEN a download intent is received THEN handleDownloadIntent must be called`() = runBlocking {
+ fun `WHEN a download intent is received THEN handleDownloadIntent must be called`() = runTest(testsDispatcher) {
val download = DownloadState("https://example.com/file.txt", "file.txt")
val downloadIntent = Intent("ACTION_DOWNLOAD")
@@ -187,7 +186,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `WHEN an intent does not provide an action THEN handleDownloadIntent must be called`() = runBlocking {
+ fun `WHEN an intent does not provide an action THEN handleDownloadIntent must be called`() = runTest(testsDispatcher) {
val download = DownloadState("https://example.com/file.txt", "file.txt")
val downloadIntent = Intent()
@@ -205,7 +204,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `WHEN a remove download intent is received THEN handleRemoveDownloadIntent must be called`() = runBlocking {
+ fun `WHEN a remove download intent is received THEN handleRemoveDownloadIntent must be called`() = runTest(testsDispatcher) {
val download = DownloadState("https://example.com/file.txt", "file.txt")
val downloadIntent = Intent(ACTION_REMOVE_PRIVATE_DOWNLOAD)
@@ -263,7 +262,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `service redelivers if no download extra is passed `() = runBlocking {
+ fun `service redelivers if no download extra is passed `() = runTest(testsDispatcher) {
val downloadIntent = Intent("ACTION_DOWNLOAD")
val intentCode = service.onStartCommand(downloadIntent, 0, 0)
@@ -272,7 +271,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `verifyDownload sets the download to failed if it is not complete`() = runBlocking {
+ fun `verifyDownload sets the download to failed if it is not complete`() = runTest(testsDispatcher) {
val downloadState = DownloadState(
url = "mozilla.org/mozilla.txt",
contentLength = 50L,
@@ -297,7 +296,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `verifyDownload does NOT set the download to failed if it is paused`() = runBlocking {
+ fun `verifyDownload does NOT set the download to failed if it is paused`() = runTest(testsDispatcher) {
val downloadState = DownloadState(
url = "mozilla.org/mozilla.txt",
contentLength = 50L,
@@ -322,7 +321,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `verifyDownload does NOT set the download to failed if it is complete`() = runBlocking {
+ fun `verifyDownload does NOT set the download to failed if it is complete`() = runTest(testsDispatcher) {
val downloadState = DownloadState(
url = "mozilla.org/mozilla.txt",
contentLength = 50L,
@@ -347,7 +346,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `verifyDownload does NOT set the download to failed if it is cancelled`() = runBlocking {
+ fun `verifyDownload does NOT set the download to failed if it is cancelled`() = runTest(testsDispatcher) {
val downloadState = DownloadState(
url = "mozilla.org/mozilla.txt",
contentLength = 50L,
@@ -372,7 +371,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `verifyDownload does NOT set the download to failed if it is status COMPLETED`() = runBlocking {
+ fun `verifyDownload does NOT set the download to failed if it is status COMPLETED`() = runTest(testsDispatcher) {
val downloadState = DownloadState(
url = "mozilla.org/mozilla.txt",
contentLength = 50L,
@@ -422,7 +421,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `broadcastReceiver handles ACTION_PAUSE`() = runBlocking {
+ fun `broadcastReceiver handles ACTION_PAUSE`() = runTest(testsDispatcher) {
val download = DownloadState("https://example.com/file.txt", "file.txt")
val response = Response(
"https://example.com/file.txt",
@@ -461,7 +460,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `broadcastReceiver handles ACTION_CANCEL`() = runBlocking {
+ fun `broadcastReceiver handles ACTION_CANCEL`() = runTest(testsDispatcher) {
val download = DownloadState("https://example.com/file.txt", "file.txt")
val response = Response(
"https://example.com/file.txt",
@@ -498,7 +497,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `broadcastReceiver handles ACTION_RESUME`() = runBlocking {
+ fun `broadcastReceiver handles ACTION_RESUME`() = runTest(testsDispatcher) {
val download = DownloadState("https://example.com/file.txt", "file.txt")
val downloadResponse = Response(
@@ -561,7 +560,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `broadcastReceiver handles ACTION_TRY_AGAIN`() = runBlocking {
+ fun `broadcastReceiver handles ACTION_TRY_AGAIN`() = runTest(testsDispatcher) {
val download = DownloadState("https://example.com/file.txt", "file.txt")
val response = Response(
"https://example.com/file.txt",
@@ -612,7 +611,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `download fails on a bad network response`() = runBlocking {
+ fun `download fails on a bad network response`() = runTest(testsDispatcher) {
val download = DownloadState("https://example.com/file.txt", "file.txt")
val response = Response(
"https://example.com/file.txt",
@@ -691,14 +690,15 @@ class AbstractFetchDownloadServiceTest {
service.setDownloadJobStatus(downloadJobState, DOWNLOADING)
assertEquals(DOWNLOADING, service.getDownloadJobStatus(downloadJobState))
- testDispatcher.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
// The additional notification is the summary one (the notification group).
assertEquals(2, shadowNotificationService.size())
}
@Test
- fun `onStartCommand must change status of INITIATED downloads to DOWNLOADING`() = runBlocking {
+ fun `onStartCommand must change status of INITIATED downloads to DOWNLOADING`() = runTest(testsDispatcher) {
val download = DownloadState("https://example.com/file.txt", "file.txt", status = INITIATED)
val downloadIntent = Intent("ACTION_DOWNLOAD")
@@ -715,7 +715,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `onStartCommand must change the status only for INITIATED downloads`() = runBlocking {
+ fun `onStartCommand must change the status only for INITIATED downloads`() = runTest(testsDispatcher) {
val download = DownloadState("https://example.com/file.txt", "file.txt", status = FAILED)
val downloadIntent = Intent("ACTION_DOWNLOAD")
@@ -729,7 +729,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `onStartCommand sets the notification foreground`() = runBlocking {
+ fun `onStartCommand sets the notification foreground`() = runTest(testsDispatcher) {
val download = DownloadState("https://example.com/file.txt", "file.txt")
val downloadIntent = Intent("ACTION_DOWNLOAD")
@@ -744,7 +744,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `sets the notification foreground in devices that support notification group`() = runBlocking {
+ fun `sets the notification foreground in devices that support notification group`() = runTest(testsDispatcher) {
val download = DownloadState(
id = "1", url = "https://example.com/file.txt", fileName = "file.txt",
status = DOWNLOADING
@@ -848,7 +848,7 @@ class AbstractFetchDownloadServiceTest {
@Test
@Config(sdk = [Build.VERSION_CODES.M])
- fun `updateNotificationGroup will do nothing on devices that do not support notificaiton groups`() = runBlocking {
+ fun `updateNotificationGroup will do nothing on devices that do not support notificaiton groups`() = runTest(testsDispatcher) {
val download = DownloadState(
id = "1", url = "https://example.com/file.txt", fileName = "file.txt",
status = DOWNLOADING
@@ -1074,7 +1074,8 @@ class AbstractFetchDownloadServiceTest {
service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED)
assertEquals(DownloadState.Status.PAUSED, service.getDownloadJobStatus(downloadJobState))
- testDispatcher.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
// one of the notifications it is the group notification only for devices the support it
assertEquals(2, shadowNotificationService.size())
@@ -1106,7 +1107,8 @@ class AbstractFetchDownloadServiceTest {
service.setDownloadJobStatus(downloadJobState, DownloadState.Status.COMPLETED)
assertEquals(DownloadState.Status.COMPLETED, service.getDownloadJobStatus(downloadJobState))
- testDispatcher.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
assertEquals(2, shadowNotificationService.size())
}
@@ -1137,7 +1139,8 @@ class AbstractFetchDownloadServiceTest {
service.setDownloadJobStatus(downloadJobState, FAILED)
assertEquals(FAILED, service.getDownloadJobStatus(downloadJobState))
- testDispatcher.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
// one of the notifications it is the group notification only for devices the support it
assertEquals(2, shadowNotificationService.size())
@@ -1169,14 +1172,15 @@ class AbstractFetchDownloadServiceTest {
service.setDownloadJobStatus(downloadJobState, DownloadState.Status.CANCELLED)
assertEquals(DownloadState.Status.CANCELLED, service.getDownloadJobStatus(downloadJobState))
- testDispatcher.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
// The additional notification is the summary one (the notification group).
assertEquals(1, shadowNotificationService.size())
}
@Test
- fun `job status is set to failed when an Exception is thrown while performDownload`() = runBlocking {
+ fun `job status is set to failed when an Exception is thrown while performDownload`() = runTest(testsDispatcher) {
doThrow(IOException()).`when`(client).fetch(any())
val download = DownloadState("https://example.com/file.txt", "file.txt")
@@ -1195,7 +1199,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `WHEN a download is from a private session the request must be private`() = runBlocking {
+ fun `WHEN a download is from a private session the request must be private`() = runTest(testsDispatcher) {
val response = Response(
"https://example.com/file.txt",
200,
@@ -1297,7 +1301,8 @@ class AbstractFetchDownloadServiceTest {
verify(service).performDownload(providedDownload.capture(), anyBoolean())
// Advance the clock so that the puller posts a notification.
- testDispatcher.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
// One of the notifications it is the group notification only for devices the support it
assertEquals(2, shadowNotificationService.size())
@@ -1360,7 +1365,8 @@ class AbstractFetchDownloadServiceTest {
service.setDownloadJobStatus(service.downloadJobs[download.id]!!, DownloadState.Status.PAUSED)
// Advance the clock so that the poller posts a notification.
- testDispatcher.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
assertEquals(2, shadowNotificationService.size())
// Now simulate onTaskRemoved.
@@ -1370,7 +1376,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `clearAllDownloadsNotificationsAndJobs cancels all running jobs and remove all notifications`() = runBlocking {
+ fun `clearAllDownloadsNotificationsAndJobs cancels all running jobs and remove all notifications`() = runTest(testsDispatcher) {
val download = DownloadState(
id = "1", url = "https://example.com/file.txt", fileName = "file.txt",
status = DOWNLOADING
@@ -1410,7 +1416,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `onDestroy will remove all download notifications, jobs and will call unregisterNotificationActionsReceiver`() = runBlocking {
+ fun `onDestroy will remove all download notifications, jobs and will call unregisterNotificationActionsReceiver`() = runTest(testsDispatcher) {
val service = spy(object : AbstractFetchDownloadService() {
override val httpClient = client
override val store = browserStore
@@ -1446,7 +1452,7 @@ class AbstractFetchDownloadServiceTest {
@Test
@Config(sdk = [Build.VERSION_CODES.P], shadows = [ShadowFileProvider::class])
- fun `WHEN a download is completed and the scoped storage is not used it MUST be added manually to the download system database`() = runBlockingTest {
+ fun `WHEN a download is completed and the scoped storage is not used it MUST be added manually to the download system database`() = runTest(testsDispatcher) {
val download = DownloadState(
url = "http://www.mozilla.org",
fileName = "example.apk",
@@ -1476,7 +1482,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `WHEN a download is completed and the scoped storage is used addToDownloadSystemDatabaseCompat MUST NOT be called`() = runBlockingTest {
+ fun `WHEN a download is completed and the scoped storage is used addToDownloadSystemDatabaseCompat MUST NOT be called`() = runTest(testsDispatcher) {
val download = DownloadState(
url = "http://www.mozilla.org",
fileName = "example.apk",
@@ -1556,7 +1562,7 @@ class AbstractFetchDownloadServiceTest {
@Test
@Config(sdk = [Build.VERSION_CODES.P], shadows = [ShadowFileProvider::class])
@Suppress("Deprecation")
- fun `do not pass non-http(s) url to addCompletedDownload`() = runBlockingTest {
+ fun `do not pass non-http(s) url to addCompletedDownload`() = runTest(testsDispatcher) {
val download = DownloadState(
url = "blob:moz-extension://d5ea9baa-64c9-4c3d-bb38-49308c47997c/",
fileName = "example.apk",
@@ -1582,7 +1588,7 @@ class AbstractFetchDownloadServiceTest {
@Config(sdk = [Build.VERSION_CODES.P], shadows = [ShadowFileProvider::class])
@Suppress("Deprecation")
fun `GIVEN a download that throws an exception WHEN adding to the system database THEN handle the exception`() =
- runBlockingTest {
+ runTest(testsDispatcher) {
val download = DownloadState(
url = "url",
fileName = "example.apk",
@@ -1616,7 +1622,7 @@ class AbstractFetchDownloadServiceTest {
@Test
@Config(sdk = [Build.VERSION_CODES.P], shadows = [ShadowFileProvider::class])
@Suppress("Deprecation")
- fun `pass http(s) url to addCompletedDownload`() = runBlockingTest {
+ fun `pass http(s) url to addCompletedDownload`() = runTest(testsDispatcher) {
val download = DownloadState(
url = "https://mozilla.com",
fileName = "example.apk",
@@ -1641,7 +1647,7 @@ class AbstractFetchDownloadServiceTest {
@Test
@Config(sdk = [Build.VERSION_CODES.P], shadows = [ShadowFileProvider::class])
@Suppress("Deprecation")
- fun `always call addCompletedDownload with a not empty or null mimeType`() = runBlockingTest {
+ fun `always call addCompletedDownload with a not empty or null mimeType`() = runTest(testsDispatcher) {
val service = spy(object : AbstractFetchDownloadService() {
override val httpClient = client
override val store = browserStore
@@ -1701,7 +1707,8 @@ class AbstractFetchDownloadServiceTest {
service.setDownloadJobStatus(cancelledDownloadJobState, DownloadState.Status.CANCELLED)
assertEquals(DownloadState.Status.CANCELLED, service.getDownloadJobStatus(cancelledDownloadJobState))
- testDispatcher.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
// The additional notification is the summary one (the notification group).
assertEquals(1, shadowNotificationService.size())
@@ -1720,13 +1727,14 @@ class AbstractFetchDownloadServiceTest {
service.setDownloadJobStatus(downloadJobState, DownloadState.Status.COMPLETED)
assertEquals(DownloadState.Status.COMPLETED, service.getDownloadJobStatus(downloadJobState))
- testDispatcher.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.advanceTimeBy(PROGRESS_UPDATE_INTERVAL)
+ mainDispatcher.scheduler.runCurrent()
// one of the notifications it is the group notification only for devices the support it
assertEquals(2, shadowNotificationService.size())
}
@Test
- fun `createDirectoryIfNeeded - MUST create directory when it does not exists`() = runBlocking {
+ fun `createDirectoryIfNeeded - MUST create directory when it does not exists`() = runTest(testsDispatcher) {
val download = DownloadState(destinationDirectory = Environment.DIRECTORY_DOWNLOADS, url = "")
val file = File(download.directoryPath)
@@ -1798,7 +1806,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `copyInChunks must alter download currentBytesCopied`() = runBlocking {
+ fun `copyInChunks must alter download currentBytesCopied`() = runTest(testsDispatcher) {
val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
val inputStream = mock()
@@ -1813,7 +1821,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `copyInChunks - must return ERROR_IN_STREAM_CLOSED when inStream is closed`() = runBlocking {
+ fun `copyInChunks - must return ERROR_IN_STREAM_CLOSED when inStream is closed`() = runTest(testsDispatcher) {
val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
val inputStream = mock()
@@ -1830,7 +1838,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `copyInChunks - must throw when inStream is closed and download was performed using http client`() = runBlocking {
+ fun `copyInChunks - must throw when inStream is closed and download was performed using http client`() = runTest(testsDispatcher) {
val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
val inputStream = mock()
var exceptionWasThrown = false
@@ -1852,7 +1860,7 @@ class AbstractFetchDownloadServiceTest {
}
@Test
- fun `copyInChunks - must return COMPLETED when finish copying bytes`() = runBlocking {
+ fun `copyInChunks - must return COMPLETED when finish copying bytes`() = runTest(testsDispatcher) {
val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
val inputStream = mock()
diff --git a/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadMiddlewareTest.kt b/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadMiddlewareTest.kt
index cba9577202c..66b2acbe51b 100644
--- a/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadMiddlewareTest.kt
+++ b/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadMiddlewareTest.kt
@@ -8,13 +8,6 @@ import android.app.DownloadManager.EXTRA_DOWNLOAD_ID
import android.content.Context
import android.content.Intent
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.runBlockingTest
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.DownloadAction
import mozilla.components.browser.state.action.TabListAction
@@ -32,11 +25,12 @@ import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import mozilla.components.support.test.whenever
-import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
-import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.never
@@ -48,27 +42,12 @@ import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class DownloadMiddlewareTest {
- private lateinit var dispatcher: TestCoroutineDispatcher
- private lateinit var scope: CoroutineScope
-
- @Before
- fun setUp() {
- dispatcher = TestCoroutineDispatcher()
- scope = CoroutineScope(dispatcher)
-
- Dispatchers.setMain(dispatcher)
- }
-
- @After
- fun tearDown() {
- dispatcher.cleanupTestCoroutines()
- scope.cancel()
-
- Dispatchers.resetMain()
- }
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
@Test
- fun `service is started when download is queued`() = runBlockingTest {
+ fun `service is started when download is queued`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadMiddleware = spy(
DownloadMiddleware(
@@ -104,7 +83,7 @@ class DownloadMiddlewareTest {
}
@Test
- fun `saveDownload do not store private downloads`() = runBlockingTest {
+ fun `saveDownload do not store private downloads`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadMiddleware = spy(
DownloadMiddleware(
@@ -127,7 +106,7 @@ class DownloadMiddlewareTest {
}
@Test
- fun `restarted downloads MUST not be passed to the downloadStorage`() = runBlockingTest {
+ fun `restarted downloads MUST not be passed to the downloadStorage`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadStorage: DownloadStorage = mock()
val downloadMiddleware = DownloadMiddleware(
@@ -153,7 +132,7 @@ class DownloadMiddlewareTest {
}
@Test
- fun `previously added downloads MUST be ignored`() = runBlockingTest {
+ fun `previously added downloads MUST be ignored`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadStorage: DownloadStorage = mock()
val download = DownloadState("https://mozilla.org/download")
@@ -176,7 +155,7 @@ class DownloadMiddlewareTest {
}
@Test
- fun `RemoveDownloadAction MUST remove from the storage`() = runBlockingTest {
+ fun `RemoveDownloadAction MUST remove from the storage`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadStorage: DownloadStorage = mock()
val downloadMiddleware = DownloadMiddleware(
@@ -199,7 +178,7 @@ class DownloadMiddlewareTest {
}
@Test
- fun `RemoveAllDownloadsAction MUST remove all downloads from the storage`() = runBlockingTest {
+ fun `RemoveAllDownloadsAction MUST remove all downloads from the storage`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadStorage: DownloadStorage = mock()
val downloadMiddleware = DownloadMiddleware(
@@ -222,7 +201,7 @@ class DownloadMiddlewareTest {
}
@Test
- fun `UpdateDownloadAction MUST update the storage when changes are needed`() = runBlockingTest {
+ fun `UpdateDownloadAction MUST update the storage when changes are needed`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadStorage: DownloadStorage = mock()
val downloadMiddleware = DownloadMiddleware(
@@ -263,10 +242,9 @@ class DownloadMiddlewareTest {
}
@Test
- fun `RestoreDownloadsState MUST populate the store with items in the storage`() = runBlockingTest {
+ fun `RestoreDownloadsState MUST populate the store with items in the storage`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadStorage: DownloadStorage = mock()
- val dispatcher = TestCoroutineDispatcher()
val downloadMiddleware = DownloadMiddleware(
applicationContext,
AbstractFetchDownloadService::class.java,
@@ -285,17 +263,16 @@ class DownloadMiddlewareTest {
store.dispatch(DownloadAction.RestoreDownloadsStateAction).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
assertEquals(download, store.state.downloads.values.first())
}
@Test
- fun `private downloads MUST NOT be restored`() = runBlockingTest {
+ fun `private downloads MUST NOT be restored`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadStorage: DownloadStorage = mock()
- val dispatcher = TestCoroutineDispatcher()
val downloadMiddleware = DownloadMiddleware(
applicationContext,
AbstractFetchDownloadService::class.java,
@@ -314,14 +291,14 @@ class DownloadMiddlewareTest {
store.dispatch(DownloadAction.RestoreDownloadsStateAction).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
assertTrue(store.state.downloads.isEmpty())
}
@Test
- fun `sendDownloadIntent MUST call startForegroundService WHEN downloads are NOT COMPLETED, CANCELLED and FAILED`() = runBlockingTest {
+ fun `sendDownloadIntent MUST call startForegroundService WHEN downloads are NOT COMPLETED, CANCELLED and FAILED`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadMiddleware = spy(
DownloadMiddleware(
@@ -349,7 +326,7 @@ class DownloadMiddlewareTest {
}
@Test
- fun `WHEN RemoveAllTabsAction and RemoveAllPrivateTabsAction are received THEN removePrivateNotifications must be called`() = runBlockingTest {
+ fun `WHEN RemoveAllTabsAction and RemoveAllPrivateTabsAction are received THEN removePrivateNotifications must be called`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadMiddleware = spy(
DownloadMiddleware(
@@ -369,7 +346,7 @@ class DownloadMiddlewareTest {
actions.forEach {
store.dispatch(it).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(downloadMiddleware, times(1)).removePrivateNotifications(any())
@@ -378,7 +355,7 @@ class DownloadMiddlewareTest {
}
@Test
- fun `WHEN RemoveTabsAction is received AND there is no private tabs THEN removePrivateNotifications MUST be called`() = runBlockingTest {
+ fun `WHEN RemoveTabsAction is received AND there is no private tabs THEN removePrivateNotifications MUST be called`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadMiddleware = spy(
DownloadMiddleware(
@@ -401,7 +378,7 @@ class DownloadMiddlewareTest {
store.dispatch(TabListAction.RemoveTabsAction(listOf("test-tab1", "test-tab3"))).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(downloadMiddleware, times(1)).removePrivateNotifications(any())
@@ -409,7 +386,7 @@ class DownloadMiddlewareTest {
}
@Test
- fun `WHEN RemoveTabsAction is received AND there is a private tab THEN removePrivateNotifications MUST NOT be called`() = runBlockingTest {
+ fun `WHEN RemoveTabsAction is received AND there is a private tab THEN removePrivateNotifications MUST NOT be called`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadMiddleware = spy(
DownloadMiddleware(
@@ -432,7 +409,7 @@ class DownloadMiddlewareTest {
store.dispatch(TabListAction.RemoveTabsAction(listOf("test-tab1", "test-tab2"))).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(downloadMiddleware, times(0)).removePrivateNotifications(any())
@@ -440,7 +417,7 @@ class DownloadMiddlewareTest {
}
@Test
- fun `WHEN RemoveTabAction is received AND there is no private tabs THEN removePrivateNotifications MUST be called`() = runBlockingTest {
+ fun `WHEN RemoveTabAction is received AND there is no private tabs THEN removePrivateNotifications MUST be called`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadMiddleware = spy(
DownloadMiddleware(
@@ -463,14 +440,14 @@ class DownloadMiddlewareTest {
store.dispatch(TabListAction.RemoveTabAction("test-tab3")).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(downloadMiddleware, times(1)).removePrivateNotifications(any())
}
@Test
- fun `WHEN RemoveTabAction is received AND there is a private tab THEN removePrivateNotifications MUST NOT be called`() = runBlockingTest {
+ fun `WHEN RemoveTabAction is received AND there is a private tab THEN removePrivateNotifications MUST NOT be called`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadMiddleware = spy(
DownloadMiddleware(
@@ -493,14 +470,14 @@ class DownloadMiddlewareTest {
store.dispatch(TabListAction.RemoveTabAction("test-tab3")).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(downloadMiddleware, times(0)).removePrivateNotifications(any())
}
@Test
- fun `WHEN removeStatusBarNotification is called THEN an ACTION_REMOVE_PRIVATE_DOWNLOAD intent must be created`() = runBlockingTest {
+ fun `WHEN removeStatusBarNotification is called THEN an ACTION_REMOVE_PRIVATE_DOWNLOAD intent must be created`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadMiddleware = spy(
DownloadMiddleware(
@@ -520,7 +497,7 @@ class DownloadMiddlewareTest {
}
@Test
- fun `WHEN removePrivateNotifications is called THEN removeStatusBarNotification will be called only for private download`() = runBlockingTest {
+ fun `WHEN removePrivateNotifications is called THEN removeStatusBarNotification will be called only for private download`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadMiddleware = spy(
DownloadMiddleware(
@@ -545,7 +522,7 @@ class DownloadMiddlewareTest {
}
@Test
- fun `WHEN removePrivateNotifications is called THEN removeStatusBarNotification will be called for all private downloads`() = runBlockingTest {
+ fun `WHEN removePrivateNotifications is called THEN removeStatusBarNotification will be called for all private downloads`() = runTestOnMain {
val applicationContext: Context = mock()
val downloadMiddleware = spy(
DownloadMiddleware(
@@ -571,7 +548,7 @@ class DownloadMiddlewareTest {
}
@Test
- fun `WHEN an action for canceling a download response is received THEN a download response must be canceled`() = runBlockingTest {
+ fun `WHEN an action for canceling a download response is received THEN a download response must be canceled`() = runTestOnMain {
val response = mock()
val download = DownloadState(id = "downloadID", url = "example.com/5MB.zip", response = response)
val applicationContext: Context = mock()
@@ -594,7 +571,7 @@ class DownloadMiddlewareTest {
store.dispatch(ContentAction.UpdateDownloadAction(tab.id, download = download)).joinBlocking()
store.dispatch(ContentAction.CancelDownloadAction(tab.id, download.id)).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(downloadMiddleware, times(1)).closeDownloadResponse(any(), any())
diff --git a/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt b/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt
index ce9d6377952..c6ed60ac5e7 100644
--- a/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt
+++ b/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt
@@ -16,10 +16,7 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.selector.findTab
@@ -39,14 +36,15 @@ import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.grantPermission
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
import mozilla.components.support.test.whenever
-import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
@@ -63,14 +61,14 @@ import org.robolectric.shadows.ShadowToast
@RunWith(AndroidJUnit4::class)
class DownloadsFeatureTest {
- private val testDispatcher = TestCoroutineDispatcher()
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
private lateinit var store: BrowserStore
@Before
- @ExperimentalCoroutinesApi
fun setUp() {
- Dispatchers.setMain(testDispatcher)
store = BrowserStore(
BrowserState(
@@ -80,13 +78,6 @@ class DownloadsFeatureTest {
)
}
- @After
- @ExperimentalCoroutinesApi
- fun tearDown() {
- Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
- }
-
@Test
fun `Adding a download object will request permissions if needed`() {
val fragmentManager: FragmentManager = mock()
@@ -110,7 +101,7 @@ class DownloadsFeatureTest {
store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
.joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertTrue(requestedPermissions)
verify(fragmentManager, never()).beginTransaction()
@@ -137,7 +128,7 @@ class DownloadsFeatureTest {
store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
.joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(fragmentManager).beginTransaction()
}
@@ -188,7 +179,7 @@ class DownloadsFeatureTest {
store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
.joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(downloadManager).download(eq(download), anyString())
}
@@ -227,7 +218,7 @@ class DownloadsFeatureTest {
store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
.joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(fragmentManager, never()).beginTransaction()
@@ -471,7 +462,7 @@ class DownloadsFeatureTest {
store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
.joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(downloadManager).download(eq(download), anyString())
verify(feature).showDownloadNotSupportedError()
diff --git a/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/share/ShareDownloadFeatureTest.kt b/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/share/ShareDownloadFeatureTest.kt
index ddb90a99d8f..3cd81912174 100644
--- a/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/share/ShareDownloadFeatureTest.kt
+++ b/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/share/ShareDownloadFeatureTest.kt
@@ -8,8 +8,6 @@ import android.content.Context
import android.webkit.MimeTypeMap
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.browser.state.action.ShareInternetResourceAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.ContentState
@@ -27,6 +25,7 @@ import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -60,7 +59,8 @@ class ShareDownloadFeatureTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val testDispatcher = coroutinesTestRule.testDispatcher
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
@Before
fun setup() {
@@ -75,7 +75,7 @@ class ShareDownloadFeatureTest {
}
@Test
- fun `cleanupCache should automatically be called when this class is initialized`() = runBlocking {
+ fun `cleanupCache should automatically be called when this class is initialized`() = runTestOnMain {
val cacheDir = File(context.cacheDir, cacheDirName).also { dir ->
dir.mkdirs()
File(dir, "leftoverFile").also { file ->
@@ -106,14 +106,14 @@ class ShareDownloadFeatureTest {
shareFeature.start()
store.dispatch(action).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(shareFeature).startSharing(download)
verify(store).dispatch(ShareInternetResourceAction.ConsumeShareAction("123"))
}
@Test
- fun `cleanupCache should delete all files from the cache directory`() = runBlocking {
+ fun `cleanupCache should delete all files from the cache directory`() = runTestOnMain {
val shareFeature = spy(ShareDownloadFeature(context, mock(), mock(), null, Dispatchers.Main))
val testDir = File(context.cacheDir, cacheDirName).also { dir ->
dir.mkdirs()
@@ -131,12 +131,12 @@ class ShareDownloadFeatureTest {
}
@Test
- fun `startSharing() will download and then share the selected download`() = runBlocking {
+ fun `startSharing() will download and then share the selected download`() = runTestOnMain {
val shareFeature = spy(ShareDownloadFeature(context, mock(), mock(), null))
val shareState = ShareInternetResourceState(url = "testUrl", contentType = "contentType")
val downloadedFile = File("filePath")
doReturn(downloadedFile).`when`(shareFeature).download(any())
- shareFeature.scope = TestCoroutineScope()
+ shareFeature.scope = scope
shareFeature.startSharing(shareState)
@@ -146,14 +146,14 @@ class ShareDownloadFeatureTest {
}
@Test
- fun `startSharing() will not use a too long HTTP url as message`() = runBlocking {
+ fun `startSharing() will not use a too long HTTP url as message`() = runTestOnMain {
val shareFeature = spy(ShareDownloadFeature(context, mock(), mock(), null))
val maxSizeUrl = "a".repeat(CHARACTERS_IN_SHARE_TEXT_LIMIT)
val tooLongUrl = maxSizeUrl + 'x'
val shareState = ShareInternetResourceState(url = tooLongUrl, contentType = "contentType")
val downloadedFile = File("filePath")
doReturn(downloadedFile).`when`(shareFeature).download(any())
- shareFeature.scope = TestCoroutineScope()
+ shareFeature.scope = scope
shareFeature.startSharing(shareState)
@@ -163,12 +163,12 @@ class ShareDownloadFeatureTest {
}
@Test
- fun `startSharing() will use an empty String as message for data URLs`() = runBlocking {
+ fun `startSharing() will use an empty String as message for data URLs`() = runTestOnMain {
val shareFeature = spy(ShareDownloadFeature(context, mock(), mock(), null))
val shareState = ShareInternetResourceState(url = "data:image/png;base64,longstring", contentType = "contentType")
val downloadedFile = File("filePath")
doReturn(downloadedFile).`when`(shareFeature).download(any())
- shareFeature.scope = TestCoroutineScope()
+ shareFeature.scope = scope
shareFeature.startSharing(shareState)
diff --git a/components/feature/findinpage/src/main/res/values-ia/strings.xml b/components/feature/findinpage/src/main/res/values-ia/strings.xml
index db53badfd10..065c0e9b5d1 100644
--- a/components/feature/findinpage/src/main/res/values-ia/strings.xml
+++ b/components/feature/findinpage/src/main/res/values-ia/strings.xml
@@ -2,7 +2,7 @@
- Trovar in le pagina
+ Cercar in le pagina
diff --git a/components/feature/findinpage/src/main/res/values-skr/strings.xml b/components/feature/findinpage/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..8a7e68969cc
--- /dev/null
+++ b/components/feature/findinpage/src/main/res/values-skr/strings.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ ورقے وچ لبھو
+
+
+ %1$d/%2$d
+
+
+ %2$d وچوں %1$d
+
+
+ اڳلا نتیجہ لبھو
+
+
+ پچھلا نتیجہ لبھو
+
+
+ ورقے وچ لبھݨ کوں فارغ کرو
+
+
diff --git a/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/internal/FindInPagePresenterTest.kt b/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/internal/FindInPagePresenterTest.kt
index 3e631458b24..fb4ce15a830 100644
--- a/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/internal/FindInPagePresenterTest.kt
+++ b/components/feature/findinpage/src/test/java/mozilla/components/feature/findinpage/internal/FindInPagePresenterTest.kt
@@ -4,11 +4,7 @@
package mozilla.components.feature.findinpage.internal
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.BrowserState
@@ -20,10 +16,11 @@ import mozilla.components.feature.findinpage.view.FindInPageView
import mozilla.components.support.test.any
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.`when`
@@ -33,13 +30,15 @@ import org.mockito.Mockito.verify
class FindInPagePresenterTest {
- private val testDispatcher = TestCoroutineDispatcher()
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
private lateinit var store: BrowserStore
@Before
@ExperimentalCoroutinesApi
fun setUp() {
- Dispatchers.setMain(testDispatcher)
store = BrowserStore(
BrowserState(
tabs = listOf(
@@ -50,13 +49,6 @@ class FindInPagePresenterTest {
)
}
- @After
- @ExperimentalCoroutinesApi
- fun tearDown() {
- Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
- }
-
@Test
fun `view is updated to display latest find result`() {
val view: FindInPageView = mock()
@@ -65,17 +57,17 @@ class FindInPagePresenterTest {
val result = FindResultState(0, 2, false)
store.dispatch(ContentAction.AddFindResultAction("test-tab", result)).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(view, never()).displayResult(result)
presenter.bind(store.state.selectedTab!!)
store.dispatch(ContentAction.AddFindResultAction("test-tab", result)).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(view).displayResult(result)
val result2 = FindResultState(1, 2, true)
store.dispatch(ContentAction.AddFindResultAction("test-tab", result2)).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(view).displayResult(result2)
}
@@ -87,12 +79,12 @@ class FindInPagePresenterTest {
presenter.bind(store.state.selectedTab!!)
store.dispatch(ContentAction.AddFindResultAction("test-tab", mock())).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(view, times(1)).displayResult(any())
presenter.stop()
store.dispatch(ContentAction.AddFindResultAction("test-tab", mock())).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(view, times(1)).displayResult(any())
}
diff --git a/components/feature/intent/src/test/java/mozilla/components/feature/intent/processing/TabIntentProcessorTest.kt b/components/feature/intent/src/test/java/mozilla/components/feature/intent/processing/TabIntentProcessorTest.kt
index 44aaa4f6897..d0de3b1acf5 100644
--- a/components/feature/intent/src/test/java/mozilla/components/feature/intent/processing/TabIntentProcessorTest.kt
+++ b/components/feature/intent/src/test/java/mozilla/components/feature/intent/processing/TabIntentProcessorTest.kt
@@ -9,7 +9,6 @@ import android.content.Intent
import android.nfc.NfcAdapter.ACTION_NDEF_DISCOVERED
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.engine.EngineMiddleware
@@ -51,7 +50,7 @@ class TabIntentProcessorTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val scope = TestCoroutineScope(coroutinesTestRule.testDispatcher)
+ private val scope = coroutinesTestRule.scope
private lateinit var middleware: CaptureActionsMiddleware
diff --git a/components/feature/media/src/main/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeature.kt b/components/feature/media/src/main/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeature.kt
index 4f523a109f2..0b4b4fdf38d 100644
--- a/components/feature/media/src/main/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeature.kt
+++ b/components/feature/media/src/main/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeature.kt
@@ -6,6 +6,7 @@ package mozilla.components.feature.media.fullscreen
import android.app.Activity
import android.content.pm.ActivityInfo
+import android.os.Build
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
@@ -47,14 +48,20 @@ class MediaSessionFullscreenFeature(
return
}
- when (activeState.mediaSessionState?.elementMetadata?.portrait) {
- true ->
- activity.requestedOrientation =
- ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
- false ->
- activity.requestedOrientation =
- ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
- else -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
+ if (store.state.selectedTabId == activeState.id) {
+ when (activeState.mediaSessionState?.elementMetadata?.portrait) {
+ true ->
+ activity.requestedOrientation =
+ ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
+ false ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInPictureInPictureMode) {
+ activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+ } else {
+ activity.requestedOrientation =
+ ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
+ }
+ else -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
+ }
}
}
diff --git a/components/feature/media/src/main/java/mozilla/components/feature/media/service/MediaSessionServiceDelegate.kt b/components/feature/media/src/main/java/mozilla/components/feature/media/service/MediaSessionServiceDelegate.kt
index 4240259c397..1a4d052a4b0 100644
--- a/components/feature/media/src/main/java/mozilla/components/feature/media/service/MediaSessionServiceDelegate.kt
+++ b/components/feature/media/src/main/java/mozilla/components/feature/media/service/MediaSessionServiceDelegate.kt
@@ -135,21 +135,18 @@ internal class MediaSessionServiceDelegate(
when (state.mediaSessionState?.playbackState) {
MediaSession.PlaybackState.PLAYING -> {
- noisyAudioStreamReceiver = BecomingNoisyReceiver(state.mediaSessionState?.controller)
+ registerBecomingNoisyListenerIfNeeded(state)
audioFocus.request(state.id)
- context.registerReceiver(noisyAudioStreamReceiver, intentFilter)
emitStatePlayFact()
startForegroundNotificationIfNeeded()
}
MediaSession.PlaybackState.PAUSED -> {
- noisyAudioStreamReceiver?.let {
- context.unregisterReceiver(noisyAudioStreamReceiver)
- noisyAudioStreamReceiver = null
- }
+ unregisterBecomingNoisyListenerIfNeeded()
emitStatePauseFact()
stopForeground()
}
else -> {
+ unregisterBecomingNoisyListenerIfNeeded()
emitStateStopFact()
stopForeground()
}
@@ -196,6 +193,22 @@ internal class MediaSessionServiceDelegate(
isForegroundService = false
}
+ private fun registerBecomingNoisyListenerIfNeeded(state: SessionState) {
+ if (noisyAudioStreamReceiver != null) {
+ return
+ }
+
+ noisyAudioStreamReceiver = BecomingNoisyReceiver(state.mediaSessionState?.controller)
+ context.registerReceiver(noisyAudioStreamReceiver, intentFilter)
+ }
+
+ private fun unregisterBecomingNoisyListenerIfNeeded() {
+ noisyAudioStreamReceiver?.let {
+ context.unregisterReceiver(noisyAudioStreamReceiver)
+ noisyAudioStreamReceiver = null
+ }
+ }
+
private suspend fun updateNotification(sessionState: SessionState?) {
val notification = notification.create(sessionState, mediaSession)
diff --git a/components/feature/media/src/main/res/values-ast/strings.xml b/components/feature/media/src/main/res/values-ast/strings.xml
index ce308b4e07f..a1b5a656f5e 100644
--- a/components/feature/media/src/main/res/values-ast/strings.xml
+++ b/components/feature/media/src/main/res/values-ast/strings.xml
@@ -2,7 +2,7 @@
- Multimedia
+ Conteníu multimediaLa cámara ta activada
@@ -18,5 +18,5 @@
Posar
- Un sitiu ta reproduciendo multimedia
+ Un sitiu ta reproduciendo conteníu multimedia
diff --git a/components/feature/media/src/main/res/values-skr/strings.xml b/components/feature/media/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..1a74ae5c77c
--- /dev/null
+++ b/components/feature/media/src/main/res/values-skr/strings.xml
@@ -0,0 +1,22 @@
+
+
+
+ میڈیا
+
+
+ کیمرہ چالو ہے
+
+ مائیکروفون چالو ہے
+
+
+ کیمرہ تے مائیکروفون چالو ہن
+
+
+ چلاؤ
+
+
+ ذرا روکو
+
+
+ سائٹ میڈیا چلیندی پئی ہے۔
+
diff --git a/components/feature/media/src/test/java/mozilla/components/feature/media/MediaSessionFeatureTest.kt b/components/feature/media/src/test/java/mozilla/components/feature/media/MediaSessionFeatureTest.kt
index 27e4fdf01a4..284c87d7cf6 100644
--- a/components/feature/media/src/test/java/mozilla/components/feature/media/MediaSessionFeatureTest.kt
+++ b/components/feature/media/src/test/java/mozilla/components/feature/media/MediaSessionFeatureTest.kt
@@ -29,7 +29,6 @@ class MediaSessionFeatureTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val dispatcher = coroutinesTestRule.testDispatcher
@Test
fun `feature triggers foreground service when there's is media session state`() {
@@ -103,7 +102,6 @@ class MediaSessionFeatureTest {
store.dispatch(MediaSessionAction.ActivatedMediaSessionAction(store.state.tabs[0].id, mock()))
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
verify(mockApplicationContext, never()).startForegroundService(any())
store.dispatch(
@@ -113,7 +111,6 @@ class MediaSessionFeatureTest {
)
)
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
}
@Test
@@ -143,7 +140,6 @@ class MediaSessionFeatureTest {
store.dispatch(MediaSessionAction.ActivatedMediaSessionAction(store.state.tabs[0].id, mock()))
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
verify(mockApplicationContext, never()).startForegroundService(any())
store.dispatch(
@@ -153,7 +149,6 @@ class MediaSessionFeatureTest {
)
)
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
verify(mockApplicationContext, times(1)).startForegroundService(any())
store.dispatch(
@@ -163,17 +158,14 @@ class MediaSessionFeatureTest {
)
)
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
verify(mockApplicationContext, times(1)).startForegroundService(any())
store.dispatch(MediaSessionAction.DeactivatedMediaSessionAction(store.state.tabs[0].id))
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
verify(mockApplicationContext, times(1)).startForegroundService(any())
store.dispatch(MediaSessionAction.ActivatedMediaSessionAction(store.state.tabs[0].id, mock()))
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
verify(mockApplicationContext, times(1)).startForegroundService(any())
store.dispatch(
@@ -183,7 +175,6 @@ class MediaSessionFeatureTest {
)
)
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
verify(mockApplicationContext, times(1)).startForegroundService(any())
store.dispatch(
@@ -193,7 +184,6 @@ class MediaSessionFeatureTest {
)
)
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
verify(mockApplicationContext, times(2)).startForegroundService(any())
store.dispatch(
@@ -203,7 +193,6 @@ class MediaSessionFeatureTest {
)
)
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
verify(mockApplicationContext, times(2)).startForegroundService(any())
}
}
diff --git a/components/feature/media/src/test/java/mozilla/components/feature/media/ext/SessionStateKtTest.kt b/components/feature/media/src/test/java/mozilla/components/feature/media/ext/SessionStateKtTest.kt
index 4feb74f7151..e1815065e79 100644
--- a/components/feature/media/src/test/java/mozilla/components/feature/media/ext/SessionStateKtTest.kt
+++ b/components/feature/media/src/test/java/mozilla/components/feature/media/ext/SessionStateKtTest.kt
@@ -2,7 +2,7 @@ package mozilla.components.feature.media.ext
import android.graphics.Bitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.support.test.mock
@@ -19,39 +19,33 @@ class SessionStateKtTest {
private val getArtworkNull: (suspend () -> Bitmap?) = { null }
@Test
- fun `getNonPrivateIcon returns null when in private mode`() {
+ fun `getNonPrivateIcon returns null when in private mode`() = runTest {
val sessionState: SessionState = mock()
val contentState: ContentState = mock()
whenever(sessionState.content).thenReturn(contentState)
whenever(contentState.private).thenReturn(true)
- var result: Bitmap?
- runBlocking {
- result = sessionState.getNonPrivateIcon(getArtwork)
- }
+ val result = sessionState.getNonPrivateIcon(getArtwork)
assertEquals(result, null)
}
@Test
- fun `getNonPrivateIcon returns bitmap when not in private mode`() {
+ fun `getNonPrivateIcon returns bitmap when not in private mode`() = runTest {
val sessionState: SessionState = mock()
val contentState: ContentState = mock()
whenever(sessionState.content).thenReturn(contentState)
whenever(contentState.private).thenReturn(false)
- var result: Bitmap?
- runBlocking {
- result = sessionState.getNonPrivateIcon(getArtwork)
- }
+ val result = sessionState.getNonPrivateIcon(getArtwork)
assertEquals(result, bitmap)
}
@Test
- fun `getNonPrivateIcon returns content icon when not in private mode`() {
+ fun `getNonPrivateIcon returns content icon when not in private mode`() = runTest {
val sessionState: SessionState = mock()
val contentState: ContentState = mock()
val icon: Bitmap = mock()
@@ -60,16 +54,13 @@ class SessionStateKtTest {
whenever(contentState.private).thenReturn(false)
whenever(contentState.icon).thenReturn(icon)
- var result: Bitmap?
- runBlocking {
- result = sessionState.getNonPrivateIcon(null)
- }
+ val result = sessionState.getNonPrivateIcon(null)
assertEquals(result, icon)
}
@Test
- fun `getNonPrivateIcon returns content icon when getArtwork return null`() {
+ fun `getNonPrivateIcon returns content icon when getArtwork return null`() = runTest {
val sessionState: SessionState = mock()
val contentState: ContentState = mock()
val icon: Bitmap = mock()
@@ -78,10 +69,7 @@ class SessionStateKtTest {
whenever(contentState.private).thenReturn(false)
whenever(contentState.icon).thenReturn(icon)
- var result: Bitmap?
- runBlocking {
- result = sessionState.getNonPrivateIcon(getArtworkNull)
- }
+ val result = sessionState.getNonPrivateIcon(getArtworkNull)
assertEquals(result, icon)
}
diff --git a/components/feature/media/src/test/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeatureTest.kt b/components/feature/media/src/test/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeatureTest.kt
index 962c55c2c16..bcfcef2f606 100644
--- a/components/feature/media/src/test/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeatureTest.kt
+++ b/components/feature/media/src/test/java/mozilla/components/feature/media/fullscreen/MediaSessionFullscreenFeatureTest.kt
@@ -6,8 +6,11 @@ package mozilla.components.feature.media.fullscreen
import android.app.Activity
import android.content.pm.ActivityInfo
+import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.MediaSessionAction
+import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.MediaSessionState
import mozilla.components.browser.state.state.createTab
@@ -16,26 +19,58 @@ import mozilla.components.concept.engine.mediasession.MediaSession
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify
+import org.robolectric.Robolectric
+import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
class MediaSessionFullscreenFeatureTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val dispatcher = coroutinesTestRule.testDispatcher
@Test
- fun `screen orientation is updated correctly`() {
- val mockActivity: Activity = mock()
+ fun `GIVEN the currently selected tab is not in fullscreen WHEN the feature is running THEN orientation is set to default`() {
+ val activity: Activity = mock()
val elementMetadata = MediaSession.ElementMetadata()
val initialState = BrowserState(
tabs = listOf(
createTab(
- "https://www.mozilla.org",
+ "https://www.mozilla.org", id = "tab1",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ elementMetadata = elementMetadata,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ fullscreen = false
+ )
+ )
+ ),
+ selectedTabId = "tab1"
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFullscreenFeature(
+ activity,
+ store
+ )
+
+ feature.start()
+
+ verify(activity).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER)
+ }
+
+ @Test
+ fun `GIVEN the currently selected tab plays portrait media WHEN the feature is running THEN orientation is set to portrait`() {
+ val activity: Activity = mock()
+ val elementMetadata = MediaSession.ElementMetadata(width = 360, height = 640)
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org", id = "tab1",
mediaSessionState = MediaSessionState(
mock(),
elementMetadata = elementMetadata,
@@ -43,28 +78,180 @@ class MediaSessionFullscreenFeatureTest {
fullscreen = true
)
)
- )
+ ),
+ selectedTabId = "tab1"
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFullscreenFeature(
+ activity,
+ store
+ )
+
+ feature.start()
+
+ verify(activity).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT)
+ }
+
+ @Test
+ fun `GIVEN the currently selected tab plays landscape media WHEN it enters fullscreen THEN set orientation to landscape`() {
+ val activity: Activity = mock()
+ val elementMetadata = MediaSession.ElementMetadata(width = 640, height = 360)
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org", id = "tab1",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ elementMetadata = elementMetadata,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ fullscreen = true
+ )
+ )
+ ),
+ selectedTabId = "tab1"
)
val store = BrowserStore(initialState)
val feature = MediaSessionFullscreenFeature(
- mockActivity,
+ activity,
store
)
feature.start()
- verify(mockActivity).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE)
+ verify(activity).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE)
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `GIVEN the currently selected tab plays landscape media WHEN it enters pip mode THEN set orientation to unspecified`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
+ val elementMetadata = MediaSession.ElementMetadata()
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org", id = "tab1",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ elementMetadata = elementMetadata,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ fullscreen = true
+ )
+ )
+ ),
+ selectedTabId = "tab1"
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFullscreenFeature(
+ activity,
+ store
+ )
+
+ feature.start()
+ activity.enterPictureInPictureMode()
+ store.waitUntilIdle()
+
+ assertTrue(activity.isInPictureInPictureMode)
+ store.dispatch(ContentAction.PictureInPictureChangedAction("tab1", true))
+ store.waitUntilIdle()
+
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, activity.requestedOrientation)
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `GIVEN the currently selected tab is in pip mode WHEN an external intent arrives THEN set orientation to default`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
+ val elementMetadata = MediaSession.ElementMetadata()
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org", id = "tab1",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ elementMetadata = elementMetadata,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ fullscreen = true
+ )
+ )
+ ),
+ selectedTabId = "tab1"
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFullscreenFeature(
+ activity,
+ store
+ )
+
+ feature.start()
+ activity.enterPictureInPictureMode()
+ store.waitUntilIdle()
+ store.dispatch(ContentAction.PictureInPictureChangedAction("tab1", true))
+ store.waitUntilIdle()
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, activity.requestedOrientation)
+
+ val tab2 = createTab(
+ url = "https://firefox.com", id = "tab2"
+ )
+ store.dispatch(TabListAction.AddTabAction(tab2, select = true))
store.dispatch(
MediaSessionAction.UpdateMediaFullscreenAction(
store.state.tabs[0].id,
- true,
- MediaSession.ElementMetadata(height = 1L)
+ false,
+ MediaSession.ElementMetadata()
)
)
+ store.waitUntilIdle()
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_USER, activity.requestedOrientation)
+ assertEquals(tab2.id, store.state.selectedTabId)
+ }
+
+ @Suppress("Deprecation")
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `GIVEN the currently selected tab is in pip mode WHEN it exits pip mode THEN set orientation to default`() {
+ val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
+ val elementMetadata = MediaSession.ElementMetadata()
+ val initialState = BrowserState(
+ tabs = listOf(
+ createTab(
+ "https://www.mozilla.org", id = "tab1",
+ mediaSessionState = MediaSessionState(
+ mock(),
+ elementMetadata = elementMetadata,
+ playbackState = MediaSession.PlaybackState.PLAYING,
+ fullscreen = true
+ )
+ )
+ ),
+ selectedTabId = "tab1"
+ )
+ val store = BrowserStore(initialState)
+ val feature = MediaSessionFullscreenFeature(
+ activity,
+ store
+ )
+ feature.start()
+ activity.enterPictureInPictureMode()
+ store.waitUntilIdle()
+
+ assertTrue(activity.isInPictureInPictureMode)
+ store.dispatch(ContentAction.PictureInPictureChangedAction("tab1", true))
+ store.waitUntilIdle()
+
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, activity.requestedOrientation)
+
+ store.dispatch(
+ MediaSessionAction.UpdateMediaFullscreenAction(
+ store.state.tabs[0].id,
+ false,
+ MediaSession.ElementMetadata()
+ )
+ )
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
- verify(mockActivity).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT)
+ assertEquals(ActivityInfo.SCREEN_ORIENTATION_USER, activity.requestedOrientation)
}
}
diff --git a/components/feature/media/src/test/java/mozilla/components/feature/media/notification/MediaNotificationTest.kt b/components/feature/media/src/test/java/mozilla/components/feature/media/notification/MediaNotificationTest.kt
index 94a7d6ecfd7..e3cc1a9dc6a 100644
--- a/components/feature/media/src/test/java/mozilla/components/feature/media/notification/MediaNotificationTest.kt
+++ b/components/feature/media/src/test/java/mozilla/components/feature/media/notification/MediaNotificationTest.kt
@@ -10,7 +10,7 @@ import android.content.Intent
import android.content.pm.PackageManager
import androidx.core.app.NotificationCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.MediaSessionState
import mozilla.components.browser.state.state.createTab
@@ -52,7 +52,7 @@ class MediaNotificationTest {
}
@Test
- fun `media session notification for playing state`() {
+ fun `media session notification for playing state`() = runTest {
val state = BrowserState(
tabs = listOf(
createTab(
@@ -62,9 +62,7 @@ class MediaNotificationTest {
)
)
- val notification = runBlocking {
- MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
- }
+ val notification = MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
assertEquals("https://www.mozilla.org", notification.text)
assertEquals("Mozilla", notification.title)
@@ -72,7 +70,7 @@ class MediaNotificationTest {
}
@Test
- fun `media session notification for paused state`() {
+ fun `media session notification for paused state`() = runTest {
val state = BrowserState(
tabs = listOf(
createTab(
@@ -82,9 +80,7 @@ class MediaNotificationTest {
)
)
- val notification = runBlocking {
- MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
- }
+ val notification = MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
assertEquals("https://www.mozilla.org", notification.text)
assertEquals("Mozilla", notification.title)
@@ -92,7 +88,7 @@ class MediaNotificationTest {
}
@Test
- fun `media session notification for stopped state`() {
+ fun `media session notification for stopped state`() = runTest {
val state = BrowserState(
tabs = listOf(
createTab(
@@ -102,16 +98,14 @@ class MediaNotificationTest {
)
)
- val notification = runBlocking {
- MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
- }
+ val notification = MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
assertEquals("", notification.text)
assertEquals("", notification.title)
}
@Test
- fun `media session notification for playing state in private mode`() {
+ fun `media session notification for playing state in private mode`() = runTest {
val state = BrowserState(
tabs = listOf(
createTab(
@@ -121,9 +115,7 @@ class MediaNotificationTest {
)
)
- val notification = runBlocking {
- MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
- }
+ val notification = MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
assertEquals("", notification.text)
assertEquals("A site is playing media", notification.title)
@@ -131,7 +123,7 @@ class MediaNotificationTest {
}
@Test
- fun `media session notification for paused state in private mode`() {
+ fun `media session notification for paused state in private mode`() = runTest {
val state = BrowserState(
tabs = listOf(
createTab(
@@ -141,9 +133,7 @@ class MediaNotificationTest {
)
)
- val notification = runBlocking {
- MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
- }
+ val notification = MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
assertEquals("", notification.text)
assertEquals("A site is playing media", notification.title)
@@ -151,7 +141,7 @@ class MediaNotificationTest {
}
@Test
- fun `media session notification with metadata in non private mode`() {
+ fun `media session notification with metadata in non private mode`() = runTest {
val mediaSessionState: MediaSessionState = mock()
val metadata: MediaSession.Metadata = mock()
whenever(mediaSessionState.metadata).thenReturn(metadata)
@@ -167,9 +157,7 @@ class MediaNotificationTest {
)
)
- val notification = runBlocking {
- MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
- }
+ val notification = MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
assertEquals("https://www.mozilla.org", notification.text)
assertEquals("test title", notification.title)
@@ -177,7 +165,7 @@ class MediaNotificationTest {
}
@Test
- fun `media session notification with metadata in private mode`() {
+ fun `media session notification with metadata in private mode`() = runTest {
val mediaSessionState: MediaSessionState = mock()
val metadata: MediaSession.Metadata = mock()
whenever(mediaSessionState.metadata).thenReturn(metadata)
@@ -193,9 +181,7 @@ class MediaNotificationTest {
)
)
- val notification = runBlocking {
- MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
- }
+ val notification = MediaNotification(context, AbstractMediaSessionService::class.java).create(state.tabs[0], mock())
assertEquals("", notification.text)
assertEquals("A site is playing media", notification.title)
diff --git a/components/feature/media/src/test/java/mozilla/components/feature/media/service/MediaSessionServiceDelegateTest.kt b/components/feature/media/src/test/java/mozilla/components/feature/media/service/MediaSessionServiceDelegateTest.kt
index 99159569bac..5cc2d6f270b 100644
--- a/components/feature/media/src/test/java/mozilla/components/feature/media/service/MediaSessionServiceDelegateTest.kt
+++ b/components/feature/media/src/test/java/mozilla/components/feature/media/service/MediaSessionServiceDelegateTest.kt
@@ -34,7 +34,6 @@ class MediaSessionServiceDelegateTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val dispatcher = coroutinesTestRule.testDispatcher
@Test
fun `media session state starts service in foreground`() {
@@ -113,7 +112,6 @@ class MediaSessionServiceDelegateTest {
store.dispatch(MediaSessionAction.DeactivatedMediaSessionAction(store.state.customTabs[0].id))
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
verify(service).stopSelf()
verify(delegate).shutdown()
diff --git a/components/feature/privatemode/build.gradle b/components/feature/privatemode/build.gradle
index 844adfc551d..7c801f6f055 100644
--- a/components/feature/privatemode/build.gradle
+++ b/components/feature/privatemode/build.gradle
@@ -33,6 +33,7 @@ dependencies {
implementation Dependencies.kotlin_stdlib
testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
testImplementation Dependencies.androidx_test_core
testImplementation Dependencies.androidx_test_junit
diff --git a/components/feature/privatemode/src/main/res/values-skr/strings.xml b/components/feature/privatemode/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..971f535406e
--- /dev/null
+++ b/components/feature/privatemode/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+
+
+
+ نجی براؤزنگ سیشن
+
diff --git a/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/AbstractPrivateNotificationServiceTest.kt b/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/AbstractPrivateNotificationServiceTest.kt
index 33e82325064..9c8ada41976 100644
--- a/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/AbstractPrivateNotificationServiceTest.kt
+++ b/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/AbstractPrivateNotificationServiceTest.kt
@@ -12,11 +12,7 @@ import android.content.SharedPreferences
import androidx.core.app.NotificationCompat
import androidx.core.content.getSystemService
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.LocaleAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
@@ -26,10 +22,11 @@ import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
import mozilla.components.support.test.whenever
-import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
@@ -44,14 +41,15 @@ import java.util.Locale
@RunWith(AndroidJUnit4::class)
class AbstractPrivateNotificationServiceTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
private lateinit var preferences: SharedPreferences
private lateinit var notificationManager: NotificationManager
- private val testDispatcher = TestCoroutineDispatcher()
@Before
fun setup() {
- Dispatchers.setMain(testDispatcher)
-
preferences = mock()
notificationManager = mock()
val editor = mock()
@@ -60,13 +58,6 @@ class AbstractPrivateNotificationServiceTest {
whenever(editor.putLong(anyString(), anyLong())).thenReturn(editor)
}
- @After
- @ExperimentalCoroutinesApi
- fun tearDown() {
- Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
- }
-
@Test
fun `WHEN the service is created THEN start foreground is called`() {
val service = spy(object : MockService() {
@@ -113,7 +104,7 @@ class AbstractPrivateNotificationServiceTest {
val mockLocale = Locale("English")
service.store.dispatch(LocaleAction.UpdateLocaleAction(mockLocale)).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(service).notifyLocaleChanged()
}
diff --git a/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/PrivateNotificationFeatureTest.kt b/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/PrivateNotificationFeatureTest.kt
index 2b0af6069e0..c1cdcc74563 100644
--- a/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/PrivateNotificationFeatureTest.kt
+++ b/components/feature/privatemode/src/test/java/mozilla/components/feature/privatemode/notification/PrivateNotificationFeatureTest.kt
@@ -4,7 +4,9 @@ import android.content.Context
import android.content.Intent
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.action.CustomTabListAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.createCustomTab
@@ -15,6 +17,7 @@ import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import mozilla.components.support.test.whenever
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@@ -49,13 +52,14 @@ class PrivateNotificationFeatureTest {
}
@Test
- fun `service should be started if pre-existing private session is present`() = runBlocking {
+ fun `service should be started if pre-existing private session is present`() = runTest(StandardTestDispatcher()) {
val privateSession = createTab("https://firefox.com", private = true)
val intent = argumentCaptor()
store.dispatch(TabListAction.AddTabAction(privateSession)).join()
feature.start()
+ runCurrent()
verify(context, times(1)).startService(intent.capture())
val expected = Intent(testContext, AbstractPrivateNotificationService::class.java)
@@ -64,7 +68,7 @@ class PrivateNotificationFeatureTest {
}
@Test
- fun `service should be started when private session is added`() = runBlocking {
+ fun `service should be started when private session is added`() = runTestOnMain {
val privateSession = createTab("https://firefox.com", private = true)
feature.start()
@@ -76,7 +80,7 @@ class PrivateNotificationFeatureTest {
}
@Test
- fun `service should not be started multiple times`() = runBlocking {
+ fun `service should not be started multiple times`() = runTestOnMain {
val privateSession1 = createTab("https://firefox.com", private = true)
val privateSession2 = createTab("https://mozilla.org", private = true)
@@ -90,7 +94,7 @@ class PrivateNotificationFeatureTest {
}
@Test
- fun `notification service should not be started when normal sessions are added`() = runBlocking {
+ fun `notification service should not be started when normal sessions are added`() = runTestOnMain {
val normalSession = createTab("https://firefox.com")
val customSession = createCustomTab("https://firefox.com")
@@ -106,7 +110,7 @@ class PrivateNotificationFeatureTest {
}
@Test
- fun `notification service should not be started when custom sessions are added`() = runBlocking {
+ fun `notification service should not be started when custom sessions are added`() = runTestOnMain {
val privateCustomSession = createCustomTab("https://firefox.com").let {
it.copy(content = it.content.copy(private = true))
}
diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt
index e7529ee3967..b1a7021cab6 100644
--- a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt
+++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt
@@ -13,7 +13,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.selector.findTabOrCustomTab
@@ -21,7 +20,6 @@ import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.Choice
-import mozilla.components.concept.engine.prompt.CreditCard
import mozilla.components.concept.engine.prompt.PromptRequest
import mozilla.components.concept.engine.prompt.PromptRequest.Alert
import mozilla.components.concept.engine.prompt.PromptRequest.Authentication
@@ -34,18 +32,26 @@ import mozilla.components.concept.engine.prompt.PromptRequest.MenuChoice
import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice
import mozilla.components.concept.engine.prompt.PromptRequest.Popup
import mozilla.components.concept.engine.prompt.PromptRequest.Repost
+import mozilla.components.concept.engine.prompt.PromptRequest.SaveCreditCard
import mozilla.components.concept.engine.prompt.PromptRequest.SaveLoginPrompt
+import mozilla.components.concept.engine.prompt.PromptRequest.SelectAddress
import mozilla.components.concept.engine.prompt.PromptRequest.SelectCreditCard
import mozilla.components.concept.engine.prompt.PromptRequest.SelectLoginPrompt
import mozilla.components.concept.engine.prompt.PromptRequest.Share
import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
import mozilla.components.concept.engine.prompt.PromptRequest.TextPrompt
import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.CreditCardValidationDelegate
import mozilla.components.concept.storage.Login
import mozilla.components.concept.storage.LoginEntry
import mozilla.components.concept.storage.LoginValidationDelegate
+import mozilla.components.feature.prompts.address.AddressDelegate
+import mozilla.components.feature.prompts.address.AddressPicker
+import mozilla.components.feature.prompts.address.DefaultAddressDelegate
import mozilla.components.feature.prompts.concept.SelectablePromptView
import mozilla.components.feature.prompts.creditcard.CreditCardPicker
+import mozilla.components.feature.prompts.creditcard.CreditCardSaveDialogFragment
import mozilla.components.feature.prompts.dialog.AlertDialogFragment
import mozilla.components.feature.prompts.dialog.AuthenticationDialogFragment
import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment
@@ -114,6 +120,9 @@ internal const val FRAGMENT_TAG = "mozac_feature_prompt_dialog"
* @property isCreditCardAutofillEnabled A callback invoked when credit card fields are detected in the webpage.
* If this resolves to `true` a prompt allowing the user to select the credit card details to be autocompleted
* will be shown.
+ * @property isAddressAutofillEnabled A callback invoked when address fields are detected in the webpage.
+ * If this resolves to `true` a prompt allowing the user to select the address details to be autocompleted
+ * will be shown.
* @property loginExceptionStorage An implementation of [LoginExceptions] that saves and checks origins
* the user does not want to see a save login dialog for.
* @property loginPickerView The [SelectablePromptView] used for [LoginPicker] to display a
@@ -126,6 +135,7 @@ internal const val FRAGMENT_TAG = "mozac_feature_prompt_dialog"
* the select credit card prompt.
* @property onSelectCreditCard A callback invoked when a user selects a credit card from the
* select credit card prompt.
+ * @property addressDelegate Delegate for address picker.
* @property onNeedToRequestPermissions A callback invoked when permissions
* need to be requested before a prompt (e.g. a file picker) can be displayed.
* Once the request is completed, [onPermissionsResult] needs to be invoked.
@@ -137,15 +147,18 @@ class PromptFeature private constructor(
private var customTabId: String?,
private val fragmentManager: FragmentManager,
private val shareDelegate: ShareDelegate,
+ override val creditCardValidationDelegate: CreditCardValidationDelegate? = null,
override val loginValidationDelegate: LoginValidationDelegate? = null,
private val isSaveLoginEnabled: () -> Boolean = { false },
private val isCreditCardAutofillEnabled: () -> Boolean = { false },
+ private val isAddressAutofillEnabled: () -> Boolean = { false },
override val loginExceptionStorage: LoginExceptions? = null,
private val loginPickerView: SelectablePromptView? = null,
private val onManageLogins: () -> Unit = {},
- private val creditCardPickerView: SelectablePromptView? = null,
+ private val creditCardPickerView: SelectablePromptView? = null,
private val onManageCreditCards: () -> Unit = {},
private val onSelectCreditCard: () -> Unit = {},
+ private val addressDelegate: AddressDelegate = DefaultAddressDelegate(),
onNeedToRequestPermissions: OnNeedToRequestPermissions
) : LifecycleAwareFeature,
PermissionsFeature,
@@ -175,15 +188,18 @@ class PromptFeature private constructor(
customTabId: String? = null,
fragmentManager: FragmentManager,
shareDelegate: ShareDelegate = DefaultShareDelegate(),
+ creditCardValidationDelegate: CreditCardValidationDelegate? = null,
loginValidationDelegate: LoginValidationDelegate? = null,
isSaveLoginEnabled: () -> Boolean = { false },
isCreditCardAutofillEnabled: () -> Boolean = { false },
+ isAddressAutofillEnabled: () -> Boolean = { false },
loginExceptionStorage: LoginExceptions? = null,
loginPickerView: SelectablePromptView? = null,
onManageLogins: () -> Unit = {},
- creditCardPickerView: SelectablePromptView? = null,
+ creditCardPickerView: SelectablePromptView? = null,
onManageCreditCards: () -> Unit = {},
onSelectCreditCard: () -> Unit = {},
+ addressDelegate: AddressDelegate = DefaultAddressDelegate(),
onNeedToRequestPermissions: OnNeedToRequestPermissions
) : this(
container = PromptContainer.Activity(activity),
@@ -191,16 +207,19 @@ class PromptFeature private constructor(
customTabId = customTabId,
fragmentManager = fragmentManager,
shareDelegate = shareDelegate,
+ creditCardValidationDelegate = creditCardValidationDelegate,
loginValidationDelegate = loginValidationDelegate,
isSaveLoginEnabled = isSaveLoginEnabled,
isCreditCardAutofillEnabled = isCreditCardAutofillEnabled,
+ isAddressAutofillEnabled = isAddressAutofillEnabled,
loginExceptionStorage = loginExceptionStorage,
onNeedToRequestPermissions = onNeedToRequestPermissions,
loginPickerView = loginPickerView,
onManageLogins = onManageLogins,
creditCardPickerView = creditCardPickerView,
onManageCreditCards = onManageCreditCards,
- onSelectCreditCard = onSelectCreditCard
+ onSelectCreditCard = onSelectCreditCard,
+ addressDelegate = addressDelegate
)
constructor(
@@ -209,15 +228,18 @@ class PromptFeature private constructor(
customTabId: String? = null,
fragmentManager: FragmentManager,
shareDelegate: ShareDelegate = DefaultShareDelegate(),
+ creditCardValidationDelegate: CreditCardValidationDelegate? = null,
loginValidationDelegate: LoginValidationDelegate? = null,
isSaveLoginEnabled: () -> Boolean = { false },
isCreditCardAutofillEnabled: () -> Boolean = { false },
+ isAddressAutofillEnabled: () -> Boolean = { false },
loginExceptionStorage: LoginExceptions? = null,
loginPickerView: SelectablePromptView? = null,
onManageLogins: () -> Unit = {},
- creditCardPickerView: SelectablePromptView? = null,
+ creditCardPickerView: SelectablePromptView? = null,
onManageCreditCards: () -> Unit = {},
onSelectCreditCard: () -> Unit = {},
+ addressDelegate: AddressDelegate = DefaultAddressDelegate(),
onNeedToRequestPermissions: OnNeedToRequestPermissions
) : this(
container = PromptContainer.Fragment(fragment),
@@ -225,16 +247,19 @@ class PromptFeature private constructor(
customTabId = customTabId,
fragmentManager = fragmentManager,
shareDelegate = shareDelegate,
+ creditCardValidationDelegate = creditCardValidationDelegate,
loginValidationDelegate = loginValidationDelegate,
isSaveLoginEnabled = isSaveLoginEnabled,
isCreditCardAutofillEnabled = isCreditCardAutofillEnabled,
+ isAddressAutofillEnabled = isAddressAutofillEnabled,
loginExceptionStorage = loginExceptionStorage,
onNeedToRequestPermissions = onNeedToRequestPermissions,
loginPickerView = loginPickerView,
onManageLogins = onManageLogins,
creditCardPickerView = creditCardPickerView,
onManageCreditCards = onManageCreditCards,
- onSelectCreditCard = onSelectCreditCard
+ onSelectCreditCard = onSelectCreditCard,
+ addressDelegate = addressDelegate
)
private val filePicker = FilePicker(container, store, customTabId, onNeedToRequestPermissions)
@@ -255,6 +280,19 @@ class PromptFeature private constructor(
)
}
+ @VisibleForTesting(otherwise = PRIVATE)
+ internal var addressPicker =
+ with(addressDelegate) {
+ addressPickerView?.let {
+ AddressPicker(
+ store = store,
+ addressSelectBar = it,
+ onManageAddresses = onManageAddresses,
+ sessionId = customTabId
+ )
+ }
+ }
+
override val onNeedToRequestPermissions
get() = filePicker.onNeedToRequestPermissions
@@ -276,14 +314,29 @@ class PromptFeature private constructor(
if (content.promptRequests.lastOrNull() != activePromptRequest) {
// Dismiss any active select login or credit card prompt if it does
// not match the current prompt request for the session.
- if (activePromptRequest is SelectLoginPrompt) {
- loginPicker?.dismissCurrentLoginSelect(activePromptRequest as SelectLoginPrompt)
- } else if (activePromptRequest is SaveLoginPrompt) {
- (activePrompt?.get() as? SaveLoginDialogFragment)?.dismissAllowingStateLoss()
- } else if (activePromptRequest is SelectCreditCard) {
- creditCardPicker?.dismissSelectCreditCardRequest(
- activePromptRequest as SelectCreditCard
- )
+ when (activePromptRequest) {
+ is SelectLoginPrompt -> {
+ loginPicker?.dismissCurrentLoginSelect(activePromptRequest as SelectLoginPrompt)
+ }
+ is SaveLoginPrompt -> {
+ (activePrompt?.get() as? SaveLoginDialogFragment)?.dismissAllowingStateLoss()
+ }
+ is SaveCreditCard -> {
+ (activePrompt?.get() as? CreditCardSaveDialogFragment)?.dismissAllowingStateLoss()
+ }
+ is SelectCreditCard -> {
+ creditCardPicker?.dismissSelectCreditCardRequest(
+ activePromptRequest as SelectCreditCard
+ )
+ }
+ is SelectAddress -> {
+ addressPicker?.dismissSelectAddressRequest(
+ activePromptRequest as SelectAddress
+ )
+ }
+ else -> {
+ // no-op
+ }
}
onPromptRequested(state)
@@ -415,6 +468,11 @@ class PromptFeature private constructor(
loginPicker?.handleSelectLoginRequest(promptRequest)
}
}
+ is SelectAddress -> {
+ if (isAddressAutofillEnabled() && promptRequest.addresses.isNotEmpty()) {
+ addressPicker?.handleSelectAddressRequest(promptRequest)
+ }
+ }
else -> handleDialogsRequest(promptRequest, session)
}
}
@@ -488,6 +546,7 @@ class PromptFeature private constructor(
is Share -> it.onSuccess()
+ is SaveCreditCard -> it.onConfirm(value as CreditCardEntry)
is SaveLoginPrompt -> it.onConfirm(value as LoginEntry)
is Confirm -> {
@@ -555,6 +614,9 @@ class PromptFeature private constructor(
)
}
+ /**
+ * Called from on [onPromptRequested] to handle requests for showing native dialogs.
+ */
@Suppress("ComplexMethod", "LongMethod")
@VisibleForTesting(otherwise = PRIVATE)
internal fun handleDialogsRequest(
@@ -563,16 +625,41 @@ class PromptFeature private constructor(
) {
// Requests that are handled with dialogs
val dialog = when (promptRequest) {
+ is SaveCreditCard -> {
+ if (!isCreditCardAutofillEnabled.invoke() || creditCardValidationDelegate == null) {
+ dismissDialogRequest(promptRequest, session)
+
+ if (creditCardValidationDelegate == null) {
+ logger.debug(
+ "Ignoring received SaveCreditCard because PromptFeature." +
+ "creditCardValidationDelegate is null. If you are trying to autofill " +
+ "credit cards, try attaching a CreditCardValidationDelegate to PromptFeature"
+ )
+ }
+
+ return
+ }
+
+ CreditCardSaveDialogFragment.newInstance(
+ sessionId = session.id,
+ promptRequestUID = promptRequest.uid,
+ shouldDismissOnLoad = false,
+ creditCard = promptRequest.creditCard
+ )
+ }
is SaveLoginPrompt -> {
- if (!isSaveLoginEnabled.invoke()) return
+ if (!isSaveLoginEnabled.invoke() || loginValidationDelegate == null) {
+ dismissDialogRequest(promptRequest, session)
+
+ if (loginValidationDelegate == null) {
+ logger.debug(
+ "Ignoring received SaveLoginPrompt because PromptFeature." +
+ "loginValidationDelegate is null. If you are trying to autofill logins, " +
+ "try attaching a LoginValidationDelegate to PromptFeature"
+ )
+ }
- if (loginValidationDelegate == null) {
- logger.debug(
- "Ignoring received SaveLoginPrompt because PromptFeature." +
- "loginValidationDelegate is null. If you are trying to autofill logins, " +
- "try attaching a LoginValidationDelegate to PromptFeature"
- )
return
}
@@ -764,6 +851,23 @@ class PromptFeature private constructor(
dialog.feature = this
if (canShowThisPrompt(promptRequest)) {
+ // If the ChoiceDialogFragment's choices data were updated,
+ // we need to dismiss the previous dialog
+ activePrompt?.get()?.let { promptDialog ->
+ // ChoiceDialogFragment could update their choices data,
+ // and we need to dismiss the previous UI dialog,
+ // without consuming the engine callbacks, and allow to create a new dialog with the
+ // updated data.
+ if (promptDialog is ChoiceDialogFragment &&
+ !session.content.promptRequests.any { it.uid == promptDialog.promptRequestUID }
+ ) {
+ // We want to avoid consuming the engine callbacks and allow a new dialog
+ // to be created with the updated data.
+ promptDialog.feature = null
+ promptDialog.dismiss()
+ }
+ }
+
dialog.show(fragmentManager, FRAGMENT_TAG)
activePrompt = WeakReference(dialog)
@@ -771,12 +875,20 @@ class PromptFeature private constructor(
activePromptsToDismiss.add(dialog)
}
} else {
- (promptRequest as Dismissible).onDismiss()
- store.dispatch(ContentAction.ConsumePromptRequestAction(session.id, promptRequest))
+ dismissDialogRequest(promptRequest, session)
}
promptAbuserDetector.updateJSDialogAbusedState()
}
+ /**
+ * Dismiss and consume the given prompt request for the session.
+ */
+ @VisibleForTesting
+ internal fun dismissDialogRequest(promptRequest: PromptRequest, session: SessionState) {
+ (promptRequest as Dismissible).onDismiss()
+ store.dispatch(ContentAction.ConsumePromptRequestAction(session.id, promptRequest))
+ }
+
private fun canShowThisPrompt(promptRequest: PromptRequest): Boolean {
return when (promptRequest) {
is SingleChoice,
@@ -790,6 +902,8 @@ class PromptFeature private constructor(
is SaveLoginPrompt,
is SelectLoginPrompt,
is SelectCreditCard,
+ is SaveCreditCard,
+ is SelectAddress,
is Share -> true
is Alert, is TextPrompt, is Confirm, is Repost, is Popup -> promptAbuserDetector.shouldShowMoreDialogs
}
@@ -822,6 +936,15 @@ class PromptFeature private constructor(
}
}
+ (activePromptRequest as? SelectAddress)?.let { selectAddressPrompt ->
+ addressPicker?.let { addressPicker ->
+ if (addressDelegate.addressPickerView?.asView()?.isVisible == true) {
+ addressPicker.dismissSelectAddressRequest(selectAddressPrompt)
+ result = true
+ }
+ }
+ }
+
return result
}
diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressAdapter.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressAdapter.kt
new file mode 100644
index 00000000000..f95086de126
--- /dev/null
+++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressAdapter.kt
@@ -0,0 +1,74 @@
+/* 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.feature.prompts.address
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.storage.Address
+import mozilla.components.feature.prompts.R
+
+@VisibleForTesting
+internal object AddressDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: Address, newItem: Address) =
+ oldItem.guid == newItem.guid
+
+ override fun areContentsTheSame(oldItem: Address, newItem: Address) =
+ oldItem == newItem
+}
+
+/**
+ * RecyclerView adapter for displaying address items.
+ */
+internal class AddressAdapter(
+ private val onAddressSelected: (Address) -> Unit
+) : ListAdapter(AddressDiffCallback) {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddressViewHolder {
+ val view = LayoutInflater
+ .from(parent.context)
+ .inflate(R.layout.mozac_feature_prompts_address_list_item, parent, false)
+ return AddressViewHolder(view, onAddressSelected)
+ }
+
+ override fun onBindViewHolder(holder: AddressViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+}
+
+/**
+ * View holder for a address item.
+ */
+@VisibleForTesting
+internal class AddressViewHolder(
+ itemView: View,
+ private val onAddressSelected: (Address) -> Unit
+) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
+ @VisibleForTesting
+ lateinit var address: Address
+
+ init {
+ itemView.setOnClickListener(this)
+ }
+
+ fun bind(address: Address) {
+ this.address = address
+ itemView.findViewById(R.id.address_name)?.text = address.displayFormat()
+ }
+
+ override fun onClick(v: View?) {
+ onAddressSelected(address)
+ }
+}
+
+/**
+ * Format the address details to be displayed to the user.
+ */
+fun Address.displayFormat(): String =
+ "${this.streetAddress}, ${this.addressLevel2}, ${this.addressLevel1}, ${this.postalCode}"
diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressDelegate.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressDelegate.kt
new file mode 100644
index 00000000000..038455b4ede
--- /dev/null
+++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressDelegate.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.feature.prompts.address
+
+import mozilla.components.concept.storage.Address
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+
+/**
+ * Delegate for address picker
+ */
+interface AddressDelegate {
+ /**
+ * The [SelectablePromptView] used for [AddressPicker] to display a
+ * selectable prompt list of address options.
+ */
+ val addressPickerView: SelectablePromptView?
+
+ /**
+ * Callback invoked when the user clicks "Manage addresses" from
+ * select address prompt.
+ */
+ val onManageAddresses: () -> Unit
+}
+
+/**
+ * Default implementation for address picker delegate
+ */
+class DefaultAddressDelegate(
+ override val addressPickerView: SelectablePromptView? = null,
+ override val onManageAddresses: () -> Unit = {}
+) : AddressDelegate
diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressPicker.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressPicker.kt
new file mode 100644
index 00000000000..877c2a43528
--- /dev/null
+++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressPicker.kt
@@ -0,0 +1,85 @@
+/* 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.feature.prompts.address
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.storage.Address
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+import mozilla.components.feature.prompts.consumePromptFrom
+import mozilla.components.support.base.log.logger.Logger
+
+/**
+ * Interactor that implements [SelectablePromptView.Listener] and notifies the feature about actions
+ * the user performed in the address picker.
+ *
+ * @property store The [BrowserStore] this feature should subscribe to.
+ * @property addressSelectBar The [SelectablePromptView] view into which the select address
+ * prompt will be inflated.
+ * @property onManageAddresses Callback invoked when user clicks on "Manage adresses" button from
+ * select address prompt.
+ * @property sessionId The session ID which requested the prompt.
+ */
+class AddressPicker(
+ private val store: BrowserStore,
+ private val addressSelectBar: SelectablePromptView,
+ private val onManageAddresses: () -> Unit = {},
+ private var sessionId: String? = null
+) : SelectablePromptView.Listener {
+
+ init {
+ addressSelectBar.listener = this
+ }
+
+ /**
+ * Shows the select address prompt in response to the [PromptRequest] event.
+ *
+ * @param request The [PromptRequest] containing the the address request data to be shown.
+ */
+ internal fun handleSelectAddressRequest(request: PromptRequest.SelectAddress) {
+ addressSelectBar.showPrompt(request.addresses)
+ }
+
+ /**
+ * Dismisses the active [PromptRequest.SelectAddress] request.
+ *
+ * @param promptRequest The current active [PromptRequest.SelectAddress] or null
+ * otherwise.
+ */
+ @Suppress("TooGenericExceptionCaught")
+ fun dismissSelectAddressRequest(promptRequest: PromptRequest.SelectAddress? = null) {
+ addressSelectBar.hidePrompt()
+
+ try {
+ if (promptRequest != null) {
+ promptRequest.onDismiss()
+ sessionId?.let {
+ store.dispatch(ContentAction.ConsumePromptRequestAction(it, promptRequest))
+ }
+ return
+ }
+
+ store.consumePromptFrom(sessionId) {
+ it.onDismiss()
+ }
+ } catch (e: RuntimeException) {
+ Logger.error("Can't dismiss select address prompt", e)
+ }
+ }
+
+ override fun onOptionSelect(option: Address) {
+ store.consumePromptFrom(sessionId) {
+ it.onConfirm(option)
+ }
+
+ addressSelectBar.hidePrompt()
+ }
+
+ override fun onManageOptions() {
+ onManageAddresses.invoke()
+ dismissSelectAddressRequest()
+ }
+}
diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressSelectBar.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressSelectBar.kt
new file mode 100644
index 00000000000..ca6d9362b2a
--- /dev/null
+++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/address/AddressSelectBar.kt
@@ -0,0 +1,147 @@
+/* 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.feature.prompts.address
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.util.AttributeSet
+import android.view.View
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.withStyledAttributes
+import androidx.core.view.isVisible
+import androidx.core.widget.ImageViewCompat
+import androidx.core.widget.TextViewCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import mozilla.components.concept.storage.Address
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+import mozilla.components.support.ktx.android.view.hideKeyboard
+
+/**
+ * A customizable "Select addresses" bar implementing [SelectablePromptView].
+ */
+class AddressSelectBar @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr), SelectablePromptView {
+
+ private var view: View? = null
+ private var recyclerView: RecyclerView? = null
+ private var headerView: AppCompatTextView? = null
+ private var expanderView: AppCompatImageView? = null
+ private var manageAddressesView: AppCompatTextView? = null
+ private var headerTextStyle: Int? = null
+
+ private val listAdapter = AddressAdapter { address ->
+ listener?.apply {
+ onOptionSelect(address)
+ }
+ }
+
+ override var listener: SelectablePromptView.Listener? = null
+
+ init {
+ context.withStyledAttributes(
+ attrs,
+ R.styleable.AddressSelectBar,
+ defStyleAttr,
+ 0
+ ) {
+ val textStyle =
+ getResourceId(
+ R.styleable.AddressSelectBar_mozacSelectAddressHeaderTextStyle,
+ 0
+ )
+
+ if (textStyle > 0) {
+ headerTextStyle = textStyle
+ }
+ }
+ }
+
+ override fun hidePrompt() {
+ this.isVisible = false
+ recyclerView?.isVisible = false
+ manageAddressesView?.isVisible = false
+
+ listAdapter.submitList(null)
+
+ toggleSelectAddressHeader(shouldExpand = false)
+ }
+
+ override fun showPrompt(options: List) {
+ if (view == null) {
+ view = View.inflate(context, LAYOUT_ID, this)
+ bindViews()
+ }
+
+ listAdapter.submitList(options)
+ view?.isVisible = true
+ }
+
+ private fun bindViews() {
+ recyclerView = findViewById(R.id.address_list).apply {
+ layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
+ adapter = listAdapter
+ }
+
+ headerView = findViewById(R.id.select_address_header).apply {
+ setOnClickListener {
+ toggleSelectAddressHeader(shouldExpand = recyclerView?.isVisible != true)
+ }
+
+ headerTextStyle?.let { appearance ->
+ TextViewCompat.setTextAppearance(this, appearance)
+ currentTextColor.let { color ->
+ TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(color))
+ }
+ }
+ }
+
+ expanderView =
+ findViewById(R.id.mozac_feature_address_expander).apply {
+ headerView?.currentTextColor?.let {
+ ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(it))
+ }
+ }
+
+ manageAddressesView = findViewById(R.id.manage_addresses).apply {
+ setOnClickListener {
+ listener?.onManageOptions()
+ }
+ }
+ }
+
+ /**
+ * Toggles the visibility of the list of address items in the prompt.
+ *
+ * @param shouldExpand True if the list of addresses should be displayed, false otherwise.
+ */
+ private fun toggleSelectAddressHeader(shouldExpand: Boolean) {
+ recyclerView?.isVisible = shouldExpand
+ manageAddressesView?.isVisible = shouldExpand
+
+ if (shouldExpand) {
+ view?.hideKeyboard()
+ expanderView?.rotation = ROTATE_180
+ headerView?.contentDescription =
+ context.getString(R.string.mozac_feature_prompts_collapse_address_content_description)
+ } else {
+ expanderView?.rotation = 0F
+ headerView?.contentDescription =
+ context.getString(R.string.mozac_feature_prompts_expand_address_content_description)
+ }
+ }
+
+ companion object {
+ val LAYOUT_ID = R.layout.mozac_feature_prompts_address_select_prompt
+
+ private const val ROTATE_180 = 180F
+ }
+}
diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolder.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolder.kt
index 4f2749cd404..a1a89840538 100644
--- a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolder.kt
+++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolder.kt
@@ -8,12 +8,9 @@ import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
-import mozilla.components.concept.engine.prompt.CreditCard
+import mozilla.components.concept.storage.CreditCardEntry
import mozilla.components.feature.prompts.R
import mozilla.components.support.utils.creditCardIssuerNetwork
-import java.text.SimpleDateFormat
-import java.util.Calendar
-import java.util.Locale
/**
* View holder for displaying a credit card item.
@@ -22,48 +19,30 @@ import java.util.Locale
*/
class CreditCardItemViewHolder(
view: View,
- private val onCreditCardSelected: (CreditCard) -> Unit
+ private val onCreditCardSelected: (CreditCardEntry) -> Unit
) : RecyclerView.ViewHolder(view) {
/**
- * Binds the view with the provided [CreditCard].
+ * Binds the view with the provided [CreditCardEntry].
*
- * @param creditCard The [CreditCard] to display.
+ * @param creditCard The [CreditCardEntry] to display.
*/
- fun bind(creditCard: CreditCard) {
+ fun bind(creditCard: CreditCardEntry) {
itemView.findViewById(R.id.credit_card_logo)
.setImageResource(creditCard.cardType.creditCardIssuerNetwork().icon)
itemView.findViewById(R.id.credit_card_number).text =
creditCard.obfuscatedCardNumber
- bindCreditCardExpiryDate(creditCard)
+ itemView.findViewById(R.id.credit_card_expiration_date).text =
+ creditCard.expiryDate
itemView.setOnClickListener {
onCreditCardSelected(creditCard)
}
}
- /**
- * Set the credit card expiry date formatted according to the locale.
- */
- private fun bindCreditCardExpiryDate(creditCard: CreditCard) {
- val dateFormat = SimpleDateFormat(DATE_PATTERN, Locale.getDefault())
-
- val calendar = Calendar.getInstance()
- calendar.set(Calendar.DAY_OF_MONTH, 1)
- // Subtract 1 from the expiry month since Calendar.Month is based on a 0-indexed.
- calendar.set(Calendar.MONTH, creditCard.expiryMonth.toInt() - 1)
- calendar.set(Calendar.YEAR, creditCard.expiryYear.toInt())
-
- itemView.findViewById(R.id.credit_card_expiration_date).text =
- dateFormat.format(calendar.time)
- }
-
companion object {
val LAYOUT_ID = R.layout.mozac_feature_prompts_credit_card_list_item
-
- // Date format pattern for the credit card expiry date.
- private const val DATE_PATTERN = "MM/yyyy"
}
}
diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardPicker.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardPicker.kt
index eace0537467..cc789af4a2f 100644
--- a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardPicker.kt
+++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardPicker.kt
@@ -6,8 +6,8 @@ package mozilla.components.feature.prompts.creditcard
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.state.store.BrowserStore
-import mozilla.components.concept.engine.prompt.CreditCard
import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.storage.CreditCardEntry
import mozilla.components.feature.prompts.concept.SelectablePromptView
import mozilla.components.feature.prompts.consumePromptFrom
import mozilla.components.feature.prompts.facts.emitCreditCardAutofillDismissedFact
@@ -29,11 +29,11 @@ import mozilla.components.support.base.log.logger.Logger
*/
class CreditCardPicker(
private val store: BrowserStore,
- private val creditCardSelectBar: SelectablePromptView,
+ private val creditCardSelectBar: SelectablePromptView,
private val manageCreditCardsCallback: () -> Unit = {},
private val selectCreditCardCallback: () -> Unit = {},
private var sessionId: String? = null
-) : SelectablePromptView.Listener {
+) : SelectablePromptView.Listener {
init {
creditCardSelectBar.listener = this
@@ -41,14 +41,14 @@ class CreditCardPicker(
// The selected credit card option to confirm.
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- internal var selectedCreditCard: CreditCard? = null
+ internal var selectedCreditCard: CreditCardEntry? = null
override fun onManageOptions() {
manageCreditCardsCallback.invoke()
dismissSelectCreditCardRequest()
}
- override fun onOptionSelect(option: CreditCard) {
+ override fun onOptionSelect(option: CreditCardEntry) {
selectedCreditCard = option
creditCardSelectBar.hidePrompt()
selectCreditCardCallback.invoke()
diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardSaveDialogFragment.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardSaveDialogFragment.kt
new file mode 100644
index 00000000000..b75dc8b5a74
--- /dev/null
+++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardSaveDialogFragment.kt
@@ -0,0 +1,200 @@
+/* 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.feature.prompts.creditcard
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.view.isVisible
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Dispatchers.Main
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.CreditCardValidationDelegate.Result
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.dialog.KEY_PROMPT_UID
+import mozilla.components.feature.prompts.dialog.KEY_SESSION_ID
+import mozilla.components.feature.prompts.dialog.KEY_SHOULD_DISMISS_ON_LOAD
+import mozilla.components.feature.prompts.dialog.PromptDialogFragment
+import mozilla.components.feature.prompts.facts.emitCreditCardAutofillCreatedFact
+import mozilla.components.feature.prompts.facts.emitCreditCardAutofillUpdatedFact
+import mozilla.components.support.ktx.android.view.toScope
+import mozilla.components.support.utils.creditCardIssuerNetwork
+
+private const val KEY_CREDIT_CARD = "KEY_CREDIT_CARD"
+
+/**
+ * [android.support.v4.app.DialogFragment] implementation to display a dialog that allows
+ * user to save a new credit card or update an existing credit card.
+ */
+internal class CreditCardSaveDialogFragment : PromptDialogFragment() {
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val creditCard by lazy { safeArguments.getParcelable(KEY_CREDIT_CARD)!! }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var confirmResult: Result = Result.CanBeCreated
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ return BottomSheetDialog(requireContext(), R.style.MozDialogStyle).apply {
+ setCancelable(true)
+ setOnShowListener {
+ val bottomSheet =
+ findViewById(com.google.android.material.R.id.design_bottom_sheet) as FrameLayout
+ val behavior = BottomSheetBehavior.from(bottomSheet)
+ behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ }
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return LayoutInflater.from(requireContext()).inflate(
+ R.layout.mozac_feature_prompt_save_credit_card_prompt,
+ container,
+ false
+ )
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ view.findViewById(R.id.credit_card_logo)
+ .setImageResource(creditCard.cardType.creditCardIssuerNetwork().icon)
+
+ view.findViewById(R.id.credit_card_number).text = creditCard.obfuscatedCardNumber
+ view.findViewById(R.id.credit_card_expiration_date).text = creditCard.expiryDate
+
+ view.findViewById
diff --git a/components/feature/prompts/src/main/res/values-bg/strings.xml b/components/feature/prompts/src/main/res/values-bg/strings.xml
index 963c2c996fd..1274a411904 100644
--- a/components/feature/prompts/src/main/res/values-bg/strings.xml
+++ b/components/feature/prompts/src/main/res/values-bg/strings.xml
@@ -20,6 +20,8 @@
Без запазванеБез запазване
+
+ Не сегаЗапазване
@@ -103,6 +105,6 @@
Разгъва предложения списък с банкови картиСгъва предложения списък с банкови карти
-
+
Управление на банкови карти
-
+
diff --git a/components/feature/prompts/src/main/res/values-co/strings.xml b/components/feature/prompts/src/main/res/values-co/strings.xml
index e71f62a107b..8cdd3637e36 100644
--- a/components/feature/prompts/src/main/res/values-co/strings.xml
+++ b/components/feature/prompts/src/main/res/values-co/strings.xml
@@ -20,6 +20,8 @@
Ùn arregistrà miccaÙn arregistrà mai
+
+ Micca subituArregistrà
@@ -103,6 +105,12 @@
Spiegà e carte bancarie sugeriteRipiegà e carte bancarie sugerite
-
+
Urganizà e carte bancarie
+
+ Arregistrà sta carta di manera sicura ?
+
+ Mudificà a data di scadenza di a carta ?
+
+ U numeru di a carta serà cifratu. U codice di sicurità ùn serà micca arregistratu.
diff --git a/components/feature/prompts/src/main/res/values-cs/strings.xml b/components/feature/prompts/src/main/res/values-cs/strings.xml
index 3a1c41ed807..e25e5aa8213 100644
--- a/components/feature/prompts/src/main/res/values-cs/strings.xml
+++ b/components/feature/prompts/src/main/res/values-cs/strings.xml
@@ -20,6 +20,8 @@
NeukládatNikdy neukládat
+
+ Teď neUložit
@@ -103,6 +105,12 @@
Zobrazit návrhy platebních karetSkrýt návrhy platebních karet
-
+
Správa platebních karet
+
+ Bezpečně uložit tuto kartu?
+
+ Aktualizovat platnost karty?
+
+ Číslo karty bude uložené šifrované. Bezpečnostní kód uložen nebude.
diff --git a/components/feature/prompts/src/main/res/values-cy/strings.xml b/components/feature/prompts/src/main/res/values-cy/strings.xml
index 534ea8fbf2c..b6ee2da9969 100644
--- a/components/feature/prompts/src/main/res/values-cy/strings.xml
+++ b/components/feature/prompts/src/main/res/values-cy/strings.xml
@@ -20,6 +20,8 @@
Peidio â chadwByth cadw
+
+ Nid nawrCadw
@@ -103,6 +105,12 @@
Ehangu awgrymiad y cardiau credydLleihau awgrymiad y cardiau credyd
-
+
Rheoli cardiau credyd
+
+ Cadw’r cerdyn hwn yn ddiogel?
+
+ Diweddaru dyddiad dod i ben cerdyn?
+
+ Bydd rhif y cerdyn yn cael ei amgryptio. Ni fydd y cod diogelwch yn cael ei gadw.
diff --git a/components/feature/prompts/src/main/res/values-da/strings.xml b/components/feature/prompts/src/main/res/values-da/strings.xml
index 874d90846d1..a34ad50bbd4 100644
--- a/components/feature/prompts/src/main/res/values-da/strings.xml
+++ b/components/feature/prompts/src/main/res/values-da/strings.xml
@@ -20,6 +20,8 @@
Gem ikkeGem aldrig
+
+ Ikke nuGem
@@ -103,6 +105,12 @@
Udvid foreslåede betalingskortSammenfold foreslåede betalingskort
-
+
Håndter betalingskort
+
+ Gem dette kort sikkert?
+
+ Opdater kortets udløbsdato?
+
+ Kortnummeret vil blive krypteret. Sikkerhedskoden vil ikke blive gemt.
diff --git a/components/feature/prompts/src/main/res/values-de/strings.xml b/components/feature/prompts/src/main/res/values-de/strings.xml
index b34f800300c..721df133f6f 100644
--- a/components/feature/prompts/src/main/res/values-de/strings.xml
+++ b/components/feature/prompts/src/main/res/values-de/strings.xml
@@ -28,6 +28,8 @@
Nicht speichernNie speichern
+
+ Nicht jetztSpeichern
@@ -115,6 +117,22 @@
Vorgeschlagene Kreditkarten ausklappenVorgeschlagene Kreditkarten einklappen
-
+
Kreditkarten verwalten
+
+ Soll diese Karte sicher gespeichert werden?
+
+ Ablaufdatum der Karte aktualisieren?
+
+ Die Kartennummer wird verschlüsselt. Der Sicherheitscode wird nicht gespeichert.
+
+
+
+ Adressen auswählen
+
+ Vorgeschlagene Adressen ausklappen
+
+ Vorgeschlagene Adressen einklappen
+
+ Adressen verwalten
diff --git a/components/feature/prompts/src/main/res/values-dsb/strings.xml b/components/feature/prompts/src/main/res/values-dsb/strings.xml
index acfc533832c..61e273f8684 100644
--- a/components/feature/prompts/src/main/res/values-dsb/strings.xml
+++ b/components/feature/prompts/src/main/res/values-dsb/strings.xml
@@ -20,6 +20,8 @@
NjeskładowaśNigda njeskładowaś
+
+ Nic něntoSkładowaś
@@ -103,6 +105,22 @@
Naraźone kreditowe kórty pokazaśNaraźone kreditowe kórty schowaś
-
+
Kreditowe kórty zastojaś
+
+ Toś tu kórtu wěsće składowaś?
+
+ Datum spadnjenja kórty aktualizěrowaś?
+
+ Numer kórty buźo se koděrowaś. Wěstotny kod njebuźo se składowaś.
+
+
+
+ Adrese wubraś
+
+ Naraźone adrese pokazaś
+
+ Naraźone adrese schowaś
+
+ Adrese zastojaś
diff --git a/components/feature/prompts/src/main/res/values-el/strings.xml b/components/feature/prompts/src/main/res/values-el/strings.xml
index ee30b11cd0f..e715715b5d1 100644
--- a/components/feature/prompts/src/main/res/values-el/strings.xml
+++ b/components/feature/prompts/src/main/res/values-el/strings.xml
@@ -20,6 +20,8 @@
Χωρίς αποθήκευσηΠοτέ αποθήκευση
+
+ Όχι τώραΑποθήκευση
@@ -103,6 +105,12 @@
Ανάπτυξη προτεινόμενων πιστωτικών καρτώνΣύμπτυξη προτεινόμενων πιστωτικών καρτών
-
+
Διαχείριση πιστωτικών καρτών
+
+ Ασφαλής αποθήκευση κάρτας;
+
+ Ενημέρωση ημερομηνίας λήξης κάρτας;
+
+ Ο αριθμός της κάρτας θα κρυπτογραφηθεί. Ο κωδικός ασφαλείας δεν θα αποθηκευτεί.
diff --git a/components/feature/prompts/src/main/res/values-en-rCA/strings.xml b/components/feature/prompts/src/main/res/values-en-rCA/strings.xml
index 96f6f39dba0..5059e798609 100644
--- a/components/feature/prompts/src/main/res/values-en-rCA/strings.xml
+++ b/components/feature/prompts/src/main/res/values-en-rCA/strings.xml
@@ -20,6 +20,8 @@
Don’t saveNever save
+
+ Not nowSave
@@ -103,6 +105,12 @@
Expand suggested credit cardsCollapse suggested credit cards
-
+
Manage credit cards
+
+ Securely save this card?
+
+ Update card expiration date?
+
+ Card number will be encrypted. Security code won’t be saved.
diff --git a/components/feature/prompts/src/main/res/values-en-rGB/strings.xml b/components/feature/prompts/src/main/res/values-en-rGB/strings.xml
index ed7cc5d8638..228af75ba90 100644
--- a/components/feature/prompts/src/main/res/values-en-rGB/strings.xml
+++ b/components/feature/prompts/src/main/res/values-en-rGB/strings.xml
@@ -20,6 +20,8 @@
Don’t saveNever save
+
+ Not nowSave
@@ -103,6 +105,22 @@
Expand suggested credit cardsCollapse suggested credit cards
-
+
Manage credit cards
+
+ Securely save this card?
+
+ Update card expiration date?
+
+ Card number will be encrypted. Security code won’t be saved.
+
+
+
+ Select addresses
+
+ Expand suggested addresses
+
+ Collapse suggested addresses
+
+ Manage addresses
diff --git a/components/feature/prompts/src/main/res/values-eo/strings.xml b/components/feature/prompts/src/main/res/values-eo/strings.xml
index 6cf7b9e5889..b2ac9708d84 100644
--- a/components/feature/prompts/src/main/res/values-eo/strings.xml
+++ b/components/feature/prompts/src/main/res/values-eo/strings.xml
@@ -20,6 +20,8 @@
Ne konserviNeniam konservi
+
+ Ne nunKonservi
@@ -103,6 +105,12 @@
Malfaldi sugestitajn kreditkartojnFaldi sugestitajn kreditkartojn
-
+
Administri kreditkartojn
+
+ Ĉu sekure konservi tiun ĉi kreditkarton?
+
+ Ĉu ĝisdatigi la daton de senvalidiĝo de kreditkarto?
+
+ La numero de kreditkaro estos ĉifrita. La sekureca kodo ne estos konservita.
diff --git a/components/feature/prompts/src/main/res/values-es-rAR/strings.xml b/components/feature/prompts/src/main/res/values-es-rAR/strings.xml
index 38cec7930f4..a25f27267cc 100644
--- a/components/feature/prompts/src/main/res/values-es-rAR/strings.xml
+++ b/components/feature/prompts/src/main/res/values-es-rAR/strings.xml
@@ -20,6 +20,8 @@
No guardarNo guardar nunca
+
+ No ahoraGuardar
@@ -103,6 +105,22 @@
Ampliar las tarjetas de crédito sugeridasContraer tarjetas de crédito sugeridas
-
+
Administrar tarjetas de crédito
+
+ ¿Guardar esta tarjeta de forma segura?
+
+ ¿Actualizar la fecha de vencimiento de la tarjeta?
+
+ El número de tarjeta será cifrado. El código de seguridad no se guardará.
+
+
+
+ Seleccionar direcciones
+
+ Expandir direcciones sugeridas
+
+ Contraer direcciones sugeridas
+
+ Administrar direcciones
diff --git a/components/feature/prompts/src/main/res/values-es-rCL/strings.xml b/components/feature/prompts/src/main/res/values-es-rCL/strings.xml
index 1c25669eb13..e55395fff48 100644
--- a/components/feature/prompts/src/main/res/values-es-rCL/strings.xml
+++ b/components/feature/prompts/src/main/res/values-es-rCL/strings.xml
@@ -20,6 +20,8 @@
No guardarNunca guardar
+
+ Ahora noGuardar
@@ -103,6 +105,22 @@
Expandir tarjetas de crédito sugeridasOcultar tarjetas de crédito sugeridas
-
+
Gestionar tarjetas de crédito
+
+ ¿Guardar esta tarjeta de forma segura?
+
+ ¿Actualizar la fecha de vencimiento de la tarjeta?
+
+ El número de la tarjeta será encriptado. El código de seguridad no será guardo.
+
+
+
+ Seleccionar direcciones
+
+ Expandir direcciones sugeridas
+
+ Ocultar direcciones sugeridas
+
+ Gestionar direcciones
diff --git a/components/feature/prompts/src/main/res/values-es-rES/strings.xml b/components/feature/prompts/src/main/res/values-es-rES/strings.xml
index 6fa730ad6a5..e130e491006 100644
--- a/components/feature/prompts/src/main/res/values-es-rES/strings.xml
+++ b/components/feature/prompts/src/main/res/values-es-rES/strings.xml
@@ -20,6 +20,8 @@
No guardarNo guardar nunca
+
+ Ahora noGuardar
@@ -103,6 +105,22 @@
Expandir la lista de tarjetas de crédito sugeridasContraer la lista de tarjetas de crédito sugeridas
-
+
Administrar tarjetas de crédito
+
+ ¿Guardar esta tarjeta de forma segura?
+
+ ¿Actualizar la fecha de caducidad de la tarjeta?
+
+ El número de tarjeta será cifrado. El código de seguridad no se guardará.
+
+
+
+ Seleccionar direcciones
+
+ Expandir direcciones sugeridas
+
+ Contraer direcciones sugeridas
+
+ Administrar direcciones
diff --git a/components/feature/prompts/src/main/res/values-es-rMX/strings.xml b/components/feature/prompts/src/main/res/values-es-rMX/strings.xml
index 0ed34751e9a..3738d9271c0 100644
--- a/components/feature/prompts/src/main/res/values-es-rMX/strings.xml
+++ b/components/feature/prompts/src/main/res/values-es-rMX/strings.xml
@@ -20,6 +20,8 @@
No guardarNunca guardar
+
+ Ahora noGuardar
@@ -103,6 +105,22 @@
Expandir sección de tarjetas de crédito sugeridasOcultar las tarjetas de crédito sugeridas
-
+
Administrar tarjetas de crédito
+
+ ¿Guardar esta tarjeta de forma segura?
+
+ ¿Actualizar la fecha de vencimiento de la tarjeta?
+
+ El número de tarjeta se encriptará. El código de seguridad no se guardará.
+
+
+
+ Seleccionar direcciones
+
+ Expandir direcciones sugeridas
+
+ Ocultar direcciones sugeridas
+
+ Administrar direcciones
diff --git a/components/feature/prompts/src/main/res/values-et/strings.xml b/components/feature/prompts/src/main/res/values-et/strings.xml
index 4366154cb53..16d7d1d24d9 100644
--- a/components/feature/prompts/src/main/res/values-et/strings.xml
+++ b/components/feature/prompts/src/main/res/values-et/strings.xml
@@ -20,6 +20,8 @@
Ära salvestaÄra salvesta kunagi
+
+ Mitte praeguSalvesta
@@ -103,6 +105,12 @@
Laienda soovitatud krediitkaarteAhenda soovitatud krediitkaarte
-
+
Halda krediitkaarte
+
+ Kas salvestada see kaart turvaliselt?
+
+ Kas uuendada kaardi aegumiskuupäeva?
+
+ Kaardi number krüptitakse. Turvakoodi ei salvestata.
diff --git a/components/feature/prompts/src/main/res/values-eu/strings.xml b/components/feature/prompts/src/main/res/values-eu/strings.xml
index 547528465ce..ff0c0e98e0f 100644
--- a/components/feature/prompts/src/main/res/values-eu/strings.xml
+++ b/components/feature/prompts/src/main/res/values-eu/strings.xml
@@ -20,6 +20,8 @@
Ez gordeEz gorde inoiz
+
+ Une honetan ezGorde
@@ -103,6 +105,12 @@
Zabaldu iradokitako kreditu-txartelakTolestu iradokitako kreditu-txartelak
-
+
Kudeatu kreditu-txartelak
+
+ Gorde txartela modu seguruan?
+
+ Eguneratu txartelaren iraungitze-data?
+
+ Txartel-zenbakia zifratu egingo da. Segurtasun-kodea ez da gordeko.
diff --git a/components/feature/prompts/src/main/res/values-fi/strings.xml b/components/feature/prompts/src/main/res/values-fi/strings.xml
index 39261d30c3e..5d84bdd1d2c 100644
--- a/components/feature/prompts/src/main/res/values-fi/strings.xml
+++ b/components/feature/prompts/src/main/res/values-fi/strings.xml
@@ -20,6 +20,8 @@
Älä tallennaÄlä tallenna koskaan
+
+ Ei nytTallenna
@@ -103,6 +105,22 @@
Laajenna ehdotetut luottokortitSupista ehdotetut luottokortit
-
+
Hallitse luottokortteja
+
+ Tallennetaanko tämä kortti turvallisesti?
+
+ Päivitetäänkö kortin viimeinen voimassaolopäivä?
+
+ Kortin numero salataan. Suojakoodia ei tallenneta.
+
+
+
+ Valitse osoitteet
+
+ Laajenna ehdotetut osoitteet
+
+ Supista ehdotetut osoitteet
+
+ Hallitse osoitteita
diff --git a/components/feature/prompts/src/main/res/values-fr/strings.xml b/components/feature/prompts/src/main/res/values-fr/strings.xml
index f9d9604f902..c59e26ea00e 100644
--- a/components/feature/prompts/src/main/res/values-fr/strings.xml
+++ b/components/feature/prompts/src/main/res/values-fr/strings.xml
@@ -26,6 +26,8 @@
Ne pas enregistrerNe jamais enregistrer
+
+ Pas pour cette foisEnregistrer
@@ -112,6 +114,12 @@
Développer les cartes bancaires suggéréesRéduire les cartes bancaires suggérées
-
+
Gérer les cartes bancaires
+
+ Enregistrer cette carte en toute sécurité ?
+
+ Mettre à jour la date d’expiration de la carte ?
+
+ Le numéro de carte sera chiffré. Le code de sécurité ne sera pas enregistré.
diff --git a/components/feature/prompts/src/main/res/values-fy-rNL/strings.xml b/components/feature/prompts/src/main/res/values-fy-rNL/strings.xml
index 93e7589300b..01bee4c8559 100644
--- a/components/feature/prompts/src/main/res/values-fy-rNL/strings.xml
+++ b/components/feature/prompts/src/main/res/values-fy-rNL/strings.xml
@@ -20,6 +20,8 @@
Net bewarjeNea bewarje
+
+ No netBewarje
@@ -103,6 +105,12 @@
Foarstelde creditcards útklappeFoarstelde creditcards ynklappe
-
+
Creditcards beheare
+
+ Dizze kaart feilich bewarje?
+
+ Ferrindatum kaart bywurkje?
+
+ It kaartnûmer sil fersifere wurde. De befeiligingskoade wurdt net bewarre.
diff --git a/components/feature/prompts/src/main/res/values-gd/strings.xml b/components/feature/prompts/src/main/res/values-gd/strings.xml
index ab84d819ac7..1ba8e814751 100644
--- a/components/feature/prompts/src/main/res/values-gd/strings.xml
+++ b/components/feature/prompts/src/main/res/values-gd/strings.xml
@@ -20,6 +20,8 @@
Na sàbhailNa sàbhail idir
+
+ Chan ann an-dràstaSàbhail
@@ -103,6 +105,6 @@
Leudaich na cairtean-creideis a mholamaidCo-theannaich na cairtean-creideis a mholamaid
-
+
Stiùirich na cairtean-creideis
-
+
diff --git a/components/feature/prompts/src/main/res/values-gn/strings.xml b/components/feature/prompts/src/main/res/values-gn/strings.xml
index 8f413f3765a..e2c3dec6571 100644
--- a/components/feature/prompts/src/main/res/values-gn/strings.xml
+++ b/components/feature/prompts/src/main/res/values-gn/strings.xml
@@ -20,6 +20,8 @@
Ani eñongatuAníke eñongatu
+
+ Ani ko’ág̃aÑongatu
@@ -103,6 +105,12 @@
Emyasãi kuatia’atã ñemurã rysýi je’epyreEñomi kuatia’atã ñemurã je’epyre
-
+
Eñangareko kuatia’atã ñemurãre
+
+ ¿Eñongatu ko kuatia’atã oĩ porã hag̃uáme?
+
+ ¿Embohekopyahu kuatia’atã arange paha?
+
+ Kuatia’atã papapy ipe’ahañemíta. Pe’ahañemi noñeñongatumo’ãi.
diff --git a/components/feature/prompts/src/main/res/values-hsb/strings.xml b/components/feature/prompts/src/main/res/values-hsb/strings.xml
index 48f37be16a9..f2f09fc737e 100644
--- a/components/feature/prompts/src/main/res/values-hsb/strings.xml
+++ b/components/feature/prompts/src/main/res/values-hsb/strings.xml
@@ -20,6 +20,8 @@
NjeskładowaćŽenje njeskładować
+
+ Nic nětkoSkładować
@@ -103,6 +105,22 @@
Namjetowane kreditne karty pokazaćNamjetowane kreditne karty schować
-
+
Kreditne karty rjadować
+
+ Tutu kartu wěsće składować?
+
+ Datum spadnjenja karty aktualizować?
+
+ Čisło karty budźe so zaklučować. Wěstotny kod njebudźe so składować.
+
+
+
+ Adresy wubrać
+
+ Namjetowane adresy pokazać
+
+ Namjetowane adresy schować
+
+ Adresy rjadować
diff --git a/components/feature/prompts/src/main/res/values-hu/strings.xml b/components/feature/prompts/src/main/res/values-hu/strings.xml
index cf3cbd054ac..13bfbf8cde1 100644
--- a/components/feature/prompts/src/main/res/values-hu/strings.xml
+++ b/components/feature/prompts/src/main/res/values-hu/strings.xml
@@ -20,6 +20,8 @@
Ne mentseSose mentse
+
+ Most nemMentés
@@ -103,6 +105,12 @@
Javasolt bankkártyák kibontásaJavasolt bankkártyák összecsukása
-
+
Bankkártyák kezelése
+
+ Elmenti biztonságosan ezt a kártyát?
+
+ Frissíti a kártya lejárati dátumát?
+
+ A kártyaszám titkosítva lesz. A biztonsági kód nem kerül mentésre.
diff --git a/components/feature/prompts/src/main/res/values-hy-rAM/strings.xml b/components/feature/prompts/src/main/res/values-hy-rAM/strings.xml
index a6a39bc7464..2865839bed8 100644
--- a/components/feature/prompts/src/main/res/values-hy-rAM/strings.xml
+++ b/components/feature/prompts/src/main/res/values-hy-rAM/strings.xml
@@ -20,6 +20,8 @@
ՉպահպանելԵրբեք չպահպանել
+
+ Ոչ հիմաՊահպանել
@@ -103,6 +105,12 @@
Ընդարձակել առաջարկվող բանկային քարտերըԿոծկել առաջարկվող բանկային քարտերը
-
+
Կառավարել բանկային քարտերը
+
+ Ապահով պահե՞լ այս քարտը:
+
+ Թարմացնե՞լ քարտի գործողության ժամկետը:
+
+ Քարտի համարը կկոդավորվի: Անվտանգության կոդը չի պահվի:
diff --git a/components/feature/prompts/src/main/res/values-ia/strings.xml b/components/feature/prompts/src/main/res/values-ia/strings.xml
index 9c20e3df3d8..786e3c936c9 100644
--- a/components/feature/prompts/src/main/res/values-ia/strings.xml
+++ b/components/feature/prompts/src/main/res/values-ia/strings.xml
@@ -20,6 +20,8 @@
Non salvarNon salvar mais
+
+ Non oraSalvar
@@ -104,6 +106,22 @@
Expander le cartas de credito suggeriteCollaber le cartas de credito suggerite
-
+
Gerer le cartas de credito
+
+ Securmente salveguardar iste carta?
+
+ Actualisar le data de expiration del carta?
+
+ Le numero de carta sera cryptate. Le codice de securitate non sera salvate.
+
+
+
+ Seliger adresses
+
+ Expander adresses suggerite
+
+ Collaber adresses suggerite
+
+ Gerer adresses
diff --git a/components/feature/prompts/src/main/res/values-in/strings.xml b/components/feature/prompts/src/main/res/values-in/strings.xml
index 822474ff36e..f7cfe27e6b2 100644
--- a/components/feature/prompts/src/main/res/values-in/strings.xml
+++ b/components/feature/prompts/src/main/res/values-in/strings.xml
@@ -20,6 +20,8 @@
Jangan simpanJangan pernah simpan
+
+ Tidak sekarangSimpan
@@ -103,6 +105,22 @@
Perluas kartu kredit yang disarankanCiutkan kartu kredit yang disarankan
-
+
Kelola kartu kredit
+
+ Simpan kartu ini dengan aman?
+
+ Perbarui tanggal kedaluwarsa kartu?
+
+ Nomor kartu akan dienkripsi. Kode keamanan tidak akan disimpan.
+
+
+
+ Pilih alamat
+
+ Bentangkan alamat yang disarankan
+
+ Ciutkan alamat yang disarankan
+
+ Kelola alamat
diff --git a/components/feature/prompts/src/main/res/values-is/strings.xml b/components/feature/prompts/src/main/res/values-is/strings.xml
index f7587ac080f..c89ef5c2739 100644
--- a/components/feature/prompts/src/main/res/values-is/strings.xml
+++ b/components/feature/prompts/src/main/res/values-is/strings.xml
@@ -20,6 +20,8 @@
Ekki vistaAldrei vista
+
+ Ekki núnaVista
@@ -105,6 +107,12 @@
Fletta út tillögum að greiðslukortumFella saman tillögur að greiðslukortum
-
+
Sýsla með greiðslukort
+
+ Vista þetta kort á öruggan hátt?
+
+ Uppfæra gildistíma korts?
+
+ Kortanúmer verður dulritað. Öryggiskóði verður ekki vistaður.
diff --git a/components/feature/prompts/src/main/res/values-it/strings.xml b/components/feature/prompts/src/main/res/values-it/strings.xml
index 8ab5f2225b2..908b2af95d2 100644
--- a/components/feature/prompts/src/main/res/values-it/strings.xml
+++ b/components/feature/prompts/src/main/res/values-it/strings.xml
@@ -20,6 +20,8 @@
Non salvareNon salvare mai
+
+ Non adessoSalva
@@ -103,6 +105,22 @@
Espandi l’elenco delle carte di credito suggeriteComprimi l’elenco delle carte di credito suggerite
-
+
Gestisci carte di credito
+
+ Salvare questa carta in modo sicuro?
+
+ Aggiornare la data di scadenza della carta?
+
+ Il numero della carta sarà crittato. Il codice di sicurezza non verrà salvato.
+
+
+
+ Seleziona indirizzi
+
+ Espandi gli indirizzi suggeriti
+
+ Comprimi gli indirizzi suggeriti
+
+ Gestisci indirizzi
diff --git a/components/feature/prompts/src/main/res/values-iw/strings.xml b/components/feature/prompts/src/main/res/values-iw/strings.xml
index 97ddc9e2523..17ba99ae7d3 100644
--- a/components/feature/prompts/src/main/res/values-iw/strings.xml
+++ b/components/feature/prompts/src/main/res/values-iw/strings.xml
@@ -20,6 +20,8 @@
לא לשמורלעולם לא לשמור
+
+ לא כעתלשמור
@@ -103,6 +105,22 @@
הרחבת כרטיסי האשראי המוצעיםצמצום כרטיסי האשראי המוצעים
-
+
ניהול כרטיסי אשראי
+
+ לשמור את הכרטיס הזה בצורה מאובטחת?
+
+ לעדכן את תאריך התפוגה של הכרטיס?
+
+ מספר הכרטיס יוצפן. קוד האבטחה לא יישמר.
+
+
+
+ בחירת כתובות
+
+ הרחבת הכתובות המוצעות
+
+ צמצום הכתובות המוצעות
+
+ ניהול כתובות
diff --git a/components/feature/prompts/src/main/res/values-ja/strings.xml b/components/feature/prompts/src/main/res/values-ja/strings.xml
index 01c8945fcb9..17f12d2e9a6 100644
--- a/components/feature/prompts/src/main/res/values-ja/strings.xml
+++ b/components/feature/prompts/src/main/res/values-ja/strings.xml
@@ -28,6 +28,8 @@
保存しない保存しない
+
+ 今はしない保存する
@@ -115,6 +117,22 @@
提案されたクレジットカード情報を展開する提案されたクレジットカード情報を折りたたむ
-
+
クレジットカードを管理
+
+ このカードの情報を安全に保存しますか?
+
+ カードの有効期限を更新しますか?
+
+ カード番号は暗号化されます。セキュリティコードは保存されません。
+
+
+
+ アドレスの選択
+
+ 提案されたアドレス情報を展開する
+
+ 提案されたアドレス情報を折りたたむ
+
+ アドレスの管理
diff --git a/components/feature/prompts/src/main/res/values-ka/strings.xml b/components/feature/prompts/src/main/res/values-ka/strings.xml
index 6091015716d..e2cd6f8bb97 100644
--- a/components/feature/prompts/src/main/res/values-ka/strings.xml
+++ b/components/feature/prompts/src/main/res/values-ka/strings.xml
@@ -20,6 +20,8 @@
შენახვის გარეშეარასოდეს შეინახოს
+
+ ახლა არაშენახვა
@@ -103,6 +105,12 @@
შემოთავაზებული საკრედიტო ბარათების ჩამოშლაშემოთავაზებული საკრედიტო ბარათების აკეცვა
-
+
საკრედიტო ბარათების მართვა
+
+ შეინახოს ეს ბარათი უსაფრთხოდ?
+
+ განახლდეს ბარათის ვადის თარიღი?
+
+ ბარათის ნომერი დაიშიფრება. უსაფრთხოების კოდი არ შეინახება.
diff --git a/components/feature/prompts/src/main/res/values-kab/strings.xml b/components/feature/prompts/src/main/res/values-kab/strings.xml
index a60a8d3d8bd..5a506b1ef62 100644
--- a/components/feature/prompts/src/main/res/values-kab/strings.xml
+++ b/components/feature/prompts/src/main/res/values-kab/strings.xml
@@ -20,6 +20,8 @@
Ur seklas araUrǧin sekles
+
+ Mačči turaSekles
@@ -103,6 +105,20 @@
Semɣer tikerḍiwin n usmad i d-yettusumrenFneẓ tikerḍiwin n usmad i d-yettusumren
-
+
Sefrektikarḍiwin n usmad
+
+ Asekles n tkarḍa-a s wudem aɣellsan?
+
+ Leqqem azemz n keffu n tkarḍa?
+
+ Uṭṭun n tkarḍa ad yettwawgelhen. Tangalt n tɣellist ur tettwaseklas ara.
+
+
+
+ Fren tansiwin
+
+ Fneẓ tansiwin d-yettwasumren
+
+ Sefrek tansiwin
diff --git a/components/feature/prompts/src/main/res/values-kk/strings.xml b/components/feature/prompts/src/main/res/values-kk/strings.xml
index ae3f47c548f..e479a624b9b 100644
--- a/components/feature/prompts/src/main/res/values-kk/strings.xml
+++ b/components/feature/prompts/src/main/res/values-kk/strings.xml
@@ -20,6 +20,8 @@
СақтамауЕшқашан сақтамау
+
+ Қазір емесСақтау
@@ -103,6 +105,12 @@
Ұсынылған несиелік карталарды жаюҰсынылған несиелік карталарды жию
-
+
Несиелік карталарды басқару
+
+ Бұл картаны қауіпсіз сақтау керек пе?
+
+ Картаның жарамдылық мерзімін жаңарту керек пе?
+
+ Карта нөмірі шифрленеді. Қауіпсіздік коды сақталмайды.
diff --git a/components/feature/prompts/src/main/res/values-ko/strings.xml b/components/feature/prompts/src/main/res/values-ko/strings.xml
index 796c4252def..9051fac5a47 100644
--- a/components/feature/prompts/src/main/res/values-ko/strings.xml
+++ b/components/feature/prompts/src/main/res/values-ko/strings.xml
@@ -20,6 +20,8 @@
저장 안 함저장 안 함
+
+ 나중에저장
@@ -103,6 +105,22 @@
제안된 신용 카드 펼치기제안된 신용 카드 접기
-
+
신용 카드 관리
+
+ 이 카드를 안전하게 저장하시겠습니까?
+
+ 카드 유효 기간을 업데이트하시겠습니까?
+
+ 카드 번호는 암호화됩니다. 보안 코드는 저장되지 않습니다.
+
+
+
+ 주소 선택
+
+ 제안된 주소 펼치기
+
+ 제안된 주소 접기
+
+ 주소 관리
diff --git a/components/feature/prompts/src/main/res/values-nb-rNO/strings.xml b/components/feature/prompts/src/main/res/values-nb-rNO/strings.xml
index 10f9b645a5f..6b4a5f98086 100644
--- a/components/feature/prompts/src/main/res/values-nb-rNO/strings.xml
+++ b/components/feature/prompts/src/main/res/values-nb-rNO/strings.xml
@@ -20,6 +20,8 @@
Ikke lagreLagre aldri
+
+ Ikke nåLagre
@@ -103,6 +105,12 @@
Utvid foreslåtte betalingskortSlå sammen foreslåtte betalingskort
-
+
Behandle betalingskort
+
+ Lagre dette kortet trygt?
+
+ Oppdatere kortets utløpsdato?
+
+ Kortnummer vil bli kryptert. Sikkerhetskoden blir ikke lagret.
diff --git a/components/feature/prompts/src/main/res/values-nl/strings.xml b/components/feature/prompts/src/main/res/values-nl/strings.xml
index 14df1091aeb..39aaf1cd7a7 100644
--- a/components/feature/prompts/src/main/res/values-nl/strings.xml
+++ b/components/feature/prompts/src/main/res/values-nl/strings.xml
@@ -20,6 +20,8 @@
Niet opslaanNooit opslaan
+
+ Niet nuOpslaan
@@ -103,6 +105,22 @@
Voorgestelde creditcards uitbreidenVoorgestelde creditcards inklappen
-
+
Creditcards beheren
+
+ Deze kaart veilig opslaan?
+
+ Vervaldatum kaart bijwerken?
+
+ Het kaartnummer wordt versleuteld. De beveiligingscode wordt niet opgeslagen.
+
+
+
+ Adressen selecteren
+
+ Voorgestelde adressen uitvouwen
+
+ Voorgestelde adressen inklappen
+
+ Adressen beheren
diff --git a/components/feature/prompts/src/main/res/values-nn-rNO/strings.xml b/components/feature/prompts/src/main/res/values-nn-rNO/strings.xml
index 788eae078c8..327b4adf0ad 100644
--- a/components/feature/prompts/src/main/res/values-nn-rNO/strings.xml
+++ b/components/feature/prompts/src/main/res/values-nn-rNO/strings.xml
@@ -20,6 +20,8 @@
Ikkje lagreLagre aldri
+
+ Ikkje noLagre
@@ -103,6 +105,18 @@
Utvid føreslått betalingskortMinimer føreslått betalingskort
-
+
Handter betalingskort
+
+ Lagre dette kortet trygt?
+
+ Oppdatere siste bruksdato for kortet?
+
+ Kortnummeret vil bli kryptert. Tryggingskoden vert ikkje lagra.
+
+
+
+ Vel adresser
+
+ Handsam adresser
diff --git a/components/feature/prompts/src/main/res/values-oc/strings.xml b/components/feature/prompts/src/main/res/values-oc/strings.xml
index 48104a65197..b3c3ed02926 100644
--- a/components/feature/prompts/src/main/res/values-oc/strings.xml
+++ b/components/feature/prompts/src/main/res/values-oc/strings.xml
@@ -20,6 +20,8 @@
Enregistrar pasEnregistrar pas jamai
+
+ Pas araEnregistrar
@@ -103,6 +105,22 @@
Espandir las cartas de crèdit suggeridasPlegar las cartas de crèdit suggeridas
-
+
Gerir las cartas de crèdit
+
+ Salvar d’un biais segur aquesta carta ?
+
+ Actualizar la data d’expiracion de la carta ?
+
+ Los numèros de carta son chifrats. Se gardarà pas lo còdi de seguretat.
+
+
+
+ Seleccionar adreças
+
+ Espandir las adreças suggeridas
+
+ Plegar las adreças suggeridas
+
+ Gestion de las adreças
diff --git a/components/feature/prompts/src/main/res/values-pa-rIN/strings.xml b/components/feature/prompts/src/main/res/values-pa-rIN/strings.xml
index 4b204d85b18..c0cd0abbf2c 100644
--- a/components/feature/prompts/src/main/res/values-pa-rIN/strings.xml
+++ b/components/feature/prompts/src/main/res/values-pa-rIN/strings.xml
@@ -20,6 +20,8 @@
ਨਾ ਸੰਭਾਲੋਕਦੇ ਨਾ ਸੰਭਾਲੋ
+
+ ਹੁਣੇ ਨਹੀਂਸੰਭਾਲੋ
@@ -104,6 +106,12 @@
ਸੁਝਾਏ ਗਏ ਕਰੈਡਿਟ ਕਾਰਡ ਨੂੰ ਫੈਲਾਓਸੁਝਾਏ ਗਏ ਕਰੈਡਿਟ ਕਾਰਡ ਨੂੰ ਸਮੇਟੋ
-
+
ਕਰੈਡਿਟ ਕਾਰਡਾਂ ਦਾ ਇੰਤਜ਼ਾਮ ਕਰੋ
+
+ ਇਹ ਕਾਰਡ ਨੂੰ ਸੁਰੱਖਿਅਤ ਢੰਗ ਨਾਲ ਸੰਭਾਲਣਾ ਹੈ?
+
+ ਕਾਰਡ ਦੀ ਮਿਆਦ ਪੁੱਗਣ ਦੀ ਤਾਰੀਖ ਨੂੰ ਅੱਪਡੇਟ ਕਰਨਾ ਹੈ?
+
+ ਕਾਰਡ ਨੰਬਰ ਨੂੰ ਇੰਕ੍ਰਿਪਟ ਕੀਤਾ ਜਾਵੇਗਾ। ਸੁਰੱਖਿਆ ਕੋਡ ਸੰਭਾਲਿਆ ਨਹੀਂ ਜਾਵੇਗਾ।
diff --git a/components/feature/prompts/src/main/res/values-pl/strings.xml b/components/feature/prompts/src/main/res/values-pl/strings.xml
index b959a1502a5..a55a7173477 100644
--- a/components/feature/prompts/src/main/res/values-pl/strings.xml
+++ b/components/feature/prompts/src/main/res/values-pl/strings.xml
@@ -20,6 +20,8 @@
Nie zachowujNigdy nie zachowuj
+
+ Nie terazZachowaj
@@ -103,6 +105,22 @@
Rozwiń podpowiadane karty płatniczeZwiń podpowiadane karty płatnicze
-
+
Zarządzaj kartami płatniczymi
+
+ Czy bezpiecznie zachować tę kartę?
+
+ Czy zaktualizować datę ważności karty?
+
+ Numer karty zostanie zaszyfrowany. Kod zabezpieczający nie zostanie zachowany.
+
+
+
+ Wybierz adresy
+
+ Rozwiń podpowiadane adresy
+
+ Zwiń podpowiadane adresy
+
+ Zarządzaj adresami
diff --git a/components/feature/prompts/src/main/res/values-pt-rBR/strings.xml b/components/feature/prompts/src/main/res/values-pt-rBR/strings.xml
index ebef28d09e3..5423058e6ec 100644
--- a/components/feature/prompts/src/main/res/values-pt-rBR/strings.xml
+++ b/components/feature/prompts/src/main/res/values-pt-rBR/strings.xml
@@ -20,6 +20,8 @@
Não salvarNunca salvar
+
+ Agora nãoSalvar
@@ -103,6 +105,22 @@
Expandir sugestões de cartão de créditoRecolher sugestões de cartão de crédito
-
+
Gerenciar cartões de crédito
+
+ Salvar este cartão com segurança?
+
+ Atualizar data de validade do cartão?
+
+ O número do cartão será criptografado. O código de segurança não será salvo.
+
+
+
+ Selecionar endereços
+
+ Expandir endereços sugeridos
+
+ Recolher endereços sugeridos
+
+ Gerenciar endereços
diff --git a/components/feature/prompts/src/main/res/values-pt-rPT/strings.xml b/components/feature/prompts/src/main/res/values-pt-rPT/strings.xml
index 1ccaadc9ee6..240e4ea7065 100644
--- a/components/feature/prompts/src/main/res/values-pt-rPT/strings.xml
+++ b/components/feature/prompts/src/main/res/values-pt-rPT/strings.xml
@@ -20,6 +20,8 @@
Não guardarNunca guardar
+
+ Agora nãoGuardar
@@ -103,6 +105,16 @@
Expandir os cartões de créditos sugeridosColapsar os cartões de crédito sugeridos
-
+
Gerir cartões de crédito
-
+
+ Guardar este cartão com segurança?
+
+ Atualizar a data de validade do cartão?
+
+ O número do cartão será encriptado. O código de segurança não será guardado.
+
+
+
+ Selecionar endereços
+
diff --git a/components/feature/prompts/src/main/res/values-rm/strings.xml b/components/feature/prompts/src/main/res/values-rm/strings.xml
index c72bb7ad676..504ec3c1d3b 100644
--- a/components/feature/prompts/src/main/res/values-rm/strings.xml
+++ b/components/feature/prompts/src/main/res/values-rm/strings.xml
@@ -20,6 +20,8 @@
Betg memorisarMai memorisar
+
+ Betg ussaMemorisar
@@ -103,6 +105,12 @@
Expander las cartas da credit proponidasReducir las cartas da credit proponidas
-
+
Administrar las cartas da credit
+
+ Memorisar questa carta a moda segira?
+
+ Actualisar la data da scadenza da la carta?
+
+ Il numer da la carta vegn criptà. Il code da segirezza na vegn betg memorisà.
diff --git a/components/feature/prompts/src/main/res/values-ru/strings.xml b/components/feature/prompts/src/main/res/values-ru/strings.xml
index a2893306872..13deb0d313a 100644
--- a/components/feature/prompts/src/main/res/values-ru/strings.xml
+++ b/components/feature/prompts/src/main/res/values-ru/strings.xml
@@ -20,6 +20,8 @@
Не сохранятьНикогда не сохранять
+
+ Не сейчасСохранить
@@ -103,6 +105,12 @@
Развернуть предлагаемые банковские картыСвернуть предлагаемые банковские карты
-
+
Управление банковскими картами
+
+ Сохранить надёжно эту карту?
+
+ Обновить срок действия карты?
+
+ Номер карты будет зашифрован. Код безопасности не будет сохранён.
diff --git a/components/feature/prompts/src/main/res/values-sat/strings.xml b/components/feature/prompts/src/main/res/values-sat/strings.xml
index 22943e69f57..0fea2bc0d3c 100644
--- a/components/feature/prompts/src/main/res/values-sat/strings.xml
+++ b/components/feature/prompts/src/main/res/values-sat/strings.xml
@@ -20,6 +20,8 @@
ᱟᱞᱚᱢ ᱥᱟᱺᱪᱟᱣᱜ-ᱟᱛᱤᱥ ᱦᱚᱸ ᱵᱟᱝ ᱥᱟᱺᱪᱟᱣᱜ-ᱟ
+
+ ᱱᱤᱛᱚᱜ ᱫᱚ ᱵᱟᱝᱟᱥᱟᱺᱪᱟᱣ ᱢᱮ
@@ -103,6 +105,12 @@
ᱵᱟᱛᱟᱣ ᱟᱠᱟᱱ ᱠᱨᱮᱰᱤᱴ ᱠᱟᱰ ᱡᱷᱟᱹᱞ ᱪᱷᱚᱭ ᱢᱮᱵᱟᱛᱟᱣ ᱟᱠᱟᱱ ᱠᱨᱮᱰᱤᱴ ᱠᱟᱰ ᱦᱚᱯᱚᱱ ᱪᱷᱚᱭ ᱢᱮ
-
+
ᱠᱨᱮᱰᱤᱴ ᱠᱟᱰ ᱠᱚ ᱢᱮᱱᱮᱡᱽ ᱢᱮ
+
+ ᱱᱚᱶᱟ ᱠᱟᱰ ᱫᱚ ᱨᱩᱠᱷᱤᱭᱟᱹ ᱥᱟᱹᱦᱤᱡ ᱫᱚᱦᱚᱭ ᱟᱢ ᱥᱮ ?
+
+ ᱠᱟᱰ ᱪᱟᱵᱟ ᱢᱟᱦᱟᱸ ᱦᱟᱹᱞᱤᱭᱟᱹᱭᱟᱹᱠ ᱟᱢ ᱥᱮ ?
+
+ ᱠᱟᱰ ᱱᱚᱢᱵᱚᱨ ᱫᱚ ᱨᱩᱠᱷᱤᱭᱟᱹᱜᱼᱟ ᱾ ᱨᱩᱠᱷᱤᱭᱟᱹ ᱠᱳᱰ ᱫᱚ ᱵᱟᱝ ᱥᱟᱧᱪᱟᱣᱜᱼᱟ ᱾
diff --git a/components/feature/prompts/src/main/res/values-sk/strings.xml b/components/feature/prompts/src/main/res/values-sk/strings.xml
index 16790d3bc6f..8d8a60d1c40 100644
--- a/components/feature/prompts/src/main/res/values-sk/strings.xml
+++ b/components/feature/prompts/src/main/res/values-sk/strings.xml
@@ -20,6 +20,8 @@
NeuložiťNikdy neukladať
+
+ Teraz nieUložiť
@@ -103,6 +105,12 @@
Rozbaliť navrhované platobné kartyZbaliť navrhované platobné karty
-
+
Spravovať platobné karty
+
+ Bezpečne uložiť túto kartu?
+
+ Aktualizovať dátum vypršania platnosti karty?
+
+ Číslo karty bude zašifrované. Bezpečnostný kód sa neuloží.
diff --git a/components/feature/prompts/src/main/res/values-skr/strings.xml b/components/feature/prompts/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..acc2ea684ca
--- /dev/null
+++ b/components/feature/prompts/src/main/res/values-skr/strings.xml
@@ -0,0 +1,128 @@
+
+
+
+ ٹھیک ہے
+
+ منسوخ
+
+ ایں ورقے کوں وادھوں ڈائیلاگ بݨاوݨ کنوں روکو
+
+ ٹھیک کرو
+
+ صاف کرو
+
+ سائن ان
+
+ ورتݨ ناں
+
+ پاس ورڈ
+
+ محفوظ نہ کرو
+
+ کݙاہیں وی محفوظ نہ کرو
+
+ ہݨ کائناں
+
+ محفوظ کرو
+
+ اپ ڈیٹ نہ کرو
+
+ اپ ڈیٹ کرو
+
+ پاس ورڈ خانہ خالی کائنی ہووݨاں چاہیدا
+
+ لاگ ان محفوظ کرݨ کنوں قاصر
+
+ ایہ لاگ ان محفوظ کروں؟
+
+ ایہ لاگ ان اپ ڈیٹ کروں؟
+
+ محفوظ تھئے پاس ورڈ وچ ورتݨ ناں شامل کروں؟
+
+
+ عبارت ان پُٹ خانے وچ درج کرݨ کیتے لیبل
+
+ رنگ چُݨو
+
+ اجازت ݙیوو
+
+ انکار کرو
+
+ بھلا تہاکوں پک ہے؟
+
+ بھلا تساں ایہ سائٹ چھوڑݨ چاہندے ہو؟ تھی سڳدے تہاݙا درج تھیا ڈیٹا محفوظ نہ تھیوے
+
+ راہوو
+
+ چھوڑو
+
+ ہک مہینہ چُݨو
+
+ جنورى
+
+ فرورى
+
+ مارچ
+
+ اپريل
+
+ مئی
+
+ جون
+
+ جولائى
+
+ اگست
+
+ ستمبر
+
+ اکتوبر
+
+ نومبر
+
+ دسمبر
+
+
+ لاگ ان منیج کرو
+
+
+ تجویز تھئے لاگ اناں کوں ودھاؤ
+
+ تجویز تھئے لاگ اناں کوں کٹھا کرو
+
+ تجویز تھئے لاگ ان
+
+
+ ایں سائٹ تے ڈیٹا ولدا بھیڄوں؟
+ ایں ورقے کوں تازہ کرݨ نال حالیہ عملاں دی ݙوجھی نقل بݨ سڳدی ہے۔ جیویں جو پیسیاں دی ادائیگی پٹھݨ یا ݙو واری تبصرہ کرݨ۔
+
+ ڈیٹا ولدا پٹھو
+
+ منسوخ
+
+
+
+ کریڈٹ کارڈ چݨو
+
+ تجویز تھئے کریڈٹ کارڈاں کوں ودھاؤ
+
+ تجویز تھئے کریڈٹ کارڈاں کوں کٹھا کرو
+
+ کریڈیٹ کارڈ منیج کرو
+
+ ایہ کارڈ حفاظت نال محفوظ کروں؟
+
+ کارڈ مُکݨ تریخ اپ ڈیٹ کروں؟
+
+ کارڈ نمبر دی خفیہ کاری کیتی ویسی۔ حفاظتی کوڈ محفوظ کائناں کیتا ویسی۔
+
+
+
+ پتے چݨو
+
+ تجویز تھئے پتیاں کوں ودھاؤ
+
+ تجویز تھئے پتیاں کوں کٹھا کرو
+
+ پتے منیج کرو
+
diff --git a/components/feature/prompts/src/main/res/values-sl/strings.xml b/components/feature/prompts/src/main/res/values-sl/strings.xml
index bdec89bab7b..205a0eeca51 100644
--- a/components/feature/prompts/src/main/res/values-sl/strings.xml
+++ b/components/feature/prompts/src/main/res/values-sl/strings.xml
@@ -20,6 +20,8 @@
Ne shraniNikoli ne shranjuj
+
+ Ne zdajShrani
@@ -103,6 +105,12 @@
Razširi predlagane kreditne karticeStrni predlagane kreditne kartice
-
+
Upravljanje kreditnih kartic
+
+ Želite varno shraniti to kartico?
+
+ Posodobi datum poteka veljavnosti kartice?
+
+ Številka kartice bo šifrirana. Varnostna koda ne bo shranjena.
diff --git a/components/feature/prompts/src/main/res/values-su/strings.xml b/components/feature/prompts/src/main/res/values-su/strings.xml
index 6445056e084..828c98087b3 100644
--- a/components/feature/prompts/src/main/res/values-su/strings.xml
+++ b/components/feature/prompts/src/main/res/values-su/strings.xml
@@ -20,6 +20,8 @@
Ulah diteundeunUlah diteundeun
+
+ Engké deuiTeundeun
@@ -103,6 +105,12 @@
Legaan kartu kiridit anu disarankeunTilep saran kartu kiridit
-
+
Kokolakeun kartu kiridit
+
+ Teundeun ieu kartu sacara aman?
+
+ Mutahirkeun titimangsa kadaluwarsa kartu?
+
+ Nomer kartu bakal diénkrip. Kode kaamanan moal diteundeun.
diff --git a/components/feature/prompts/src/main/res/values-sv-rSE/strings.xml b/components/feature/prompts/src/main/res/values-sv-rSE/strings.xml
index 0c2bc7e1cbe..86ee27762b8 100644
--- a/components/feature/prompts/src/main/res/values-sv-rSE/strings.xml
+++ b/components/feature/prompts/src/main/res/values-sv-rSE/strings.xml
@@ -20,6 +20,8 @@
Spara inteSpara aldrig
+
+ Inte nuSpara
@@ -103,6 +105,12 @@
Expandera föreslagna kreditkortDölj föreslagna kreditkort
-
+
Hantera kreditkort
+
+ Vill du spara det här kortet säkert?
+
+ Uppdatera kortets utgångsdatum?
+
+ Kortnummer kommer att krypteras. Säkerhetskoden kommer inte att sparas.
diff --git a/components/feature/prompts/src/main/res/values-tg/strings.xml b/components/feature/prompts/src/main/res/values-tg/strings.xml
index 75e436d3e2e..935fdf357e7 100644
--- a/components/feature/prompts/src/main/res/values-tg/strings.xml
+++ b/components/feature/prompts/src/main/res/values-tg/strings.xml
@@ -20,6 +20,8 @@
Нигоҳ дошта нашавадҲеҷ гоҳ нигоҳ дошта нашавад
+
+ Ҳоло неНигоҳ доштан
@@ -103,6 +105,22 @@
Кортҳои кредитии пешниҳодшударо нишон диҳедКортҳои кредитии пешниҳодшударо пинҳон кунед
-
+
Идоракунии кортҳои кредитӣ
+
+ Ин кортро ба таври бехатар нигоҳ медоред?
+
+ Санаи анҷоми муҳлати кори кортро нав мекунед?
+
+ Рақами корт рамзгузорӣ карда мешавад. Рамзи амният нигоҳ дошта намешавад.
+
+
+
+ Интихоб кардани нишониҳо
+
+ Намоиш додани нишониҳои пешниҳодшуда
+
+ Пинҳон кардани нишониҳои пешниҳодшуда
+
+ Идоракунии нишониҳо
diff --git a/components/feature/prompts/src/main/res/values-th/strings.xml b/components/feature/prompts/src/main/res/values-th/strings.xml
index 0d3c51167be..c2f684ea6d4 100644
--- a/components/feature/prompts/src/main/res/values-th/strings.xml
+++ b/components/feature/prompts/src/main/res/values-th/strings.xml
@@ -20,6 +20,8 @@
ไม่บันทึกไม่บันทึกเสมอ
+
+ ไม่ใช่ตอนนี้บันทึก
@@ -103,6 +105,12 @@
ขยายบัตรเครดิตที่เสนอแนะยุบบัตรเครดิตที่เสนอแนะ
-
+
จัดการบัตรเครดิต
+
+ ต้องการบันทึกบัตรนี้อย่างปลอดภัยหรือไม่?
+
+ ต้องการปรับปรุงวันหมดอายุบัตรหรือไม่?
+
+ หมายเลขบัตรจะถูกเข้ารหัส รหัสความปลอดภัยจะไม่ถูกบันทึก
diff --git a/components/feature/prompts/src/main/res/values-tr/strings.xml b/components/feature/prompts/src/main/res/values-tr/strings.xml
index 71d7013f1ac..fc0472010cd 100644
--- a/components/feature/prompts/src/main/res/values-tr/strings.xml
+++ b/components/feature/prompts/src/main/res/values-tr/strings.xml
@@ -20,6 +20,8 @@
KaydetmeAsla kaydetme
+
+ Daha sonraKaydet
@@ -103,6 +105,12 @@
Önerilen kredi kartlarını genişletÖnerilen kredi kartlarını daralt
-
+
Kredi kartlarını yönet
+
+ Bu kart güvenli bir şekilde kaydedilsin mi?
+
+ Kartın son kullanma tarihi güncellensin mi?
+
+ Kart numarası şifrelenecektir. Güvenlik kodu kaydedilmeyecektir.
diff --git a/components/feature/prompts/src/main/res/values-uk/strings.xml b/components/feature/prompts/src/main/res/values-uk/strings.xml
index 86a1592905c..22f3abe2a06 100644
--- a/components/feature/prompts/src/main/res/values-uk/strings.xml
+++ b/components/feature/prompts/src/main/res/values-uk/strings.xml
@@ -20,6 +20,8 @@
Не зберігатиНіколи не зберігати
+
+ Не заразЗберегти
@@ -103,6 +105,22 @@
Розгорнути запропоновані кредитні карткиЗгорнути запропоновані кредитні картки
-
+
Керувати кредитними картками
+
+ Зберегти надійно цю картку?
+
+ Оновити термін дії картки?
+
+ Номер картки буде зашифровано. Код безпеки не буде збережено.
+
+
+
+ Вибрати адреси
+
+ Розгорнути пропоновані адреси
+
+ Згорнути пропоновані адреси
+
+ Керувати адресами
diff --git a/components/feature/prompts/src/main/res/values-vi/strings.xml b/components/feature/prompts/src/main/res/values-vi/strings.xml
index e42773e6494..d3c7a0f5f43 100644
--- a/components/feature/prompts/src/main/res/values-vi/strings.xml
+++ b/components/feature/prompts/src/main/res/values-vi/strings.xml
@@ -20,6 +20,8 @@
Không lưuKhông bao giờ lưu
+
+ Không phải bây giờLưu
@@ -103,6 +105,22 @@
Mở rộng thẻ tín dụng được đề xuấtThu gọn thẻ tín dụng được đề xuất
-
+
Quản lý thẻ tín dụng
+
+ Lưu thẻ này một cách an toàn?
+
+ Cập nhật ngày hết hạn thẻ?
+
+ Số thẻ sẽ được mã hóa. Mã bảo mật sẽ không được lưu.
+
+
+
+ Chọn địa chỉ
+
+ Mở rộng các địa chỉ được đề xuất
+
+ Thu gọn các địa chỉ được đề xuất
+
+ Quản lý địa chỉ
diff --git a/components/feature/prompts/src/main/res/values-zh-rCN/strings.xml b/components/feature/prompts/src/main/res/values-zh-rCN/strings.xml
index f9b32933014..e2fd3929586 100644
--- a/components/feature/prompts/src/main/res/values-zh-rCN/strings.xml
+++ b/components/feature/prompts/src/main/res/values-zh-rCN/strings.xml
@@ -28,6 +28,8 @@
不保存总不保存
+
+ 暂时不要保存
@@ -115,6 +117,18 @@
展开建议的信用卡折叠建议的信用卡
-
+
管理信用卡
+
+ 安全地保存此卡片?
+
+ 是否要更新卡片失效日期?
+
+ 卡号将被加密,且不会保存安全码。
+
+
+
+ 选择地址
+
+ 管理地址
diff --git a/components/feature/prompts/src/main/res/values-zh-rTW/strings.xml b/components/feature/prompts/src/main/res/values-zh-rTW/strings.xml
index ab60f199e4c..22df445050b 100644
--- a/components/feature/prompts/src/main/res/values-zh-rTW/strings.xml
+++ b/components/feature/prompts/src/main/res/values-zh-rTW/strings.xml
@@ -28,6 +28,8 @@
不要儲存永不儲存
+
+ 現在不要儲存
@@ -115,6 +117,22 @@
展開建議的信用卡摺疊建議的信用卡
-
+
管理信用卡
+
+ 安全地儲存這張卡的資料?
+
+ 是否要更新卡片效期?
+
+ 將加密卡號,也不會儲存安全碼。
+
+
+
+ 選擇地址
+
+ 展開建議的地址
+
+ 摺疊建議的地址
+
+ 管理已存地址
diff --git a/components/feature/prompts/src/main/res/values/attrs.xml b/components/feature/prompts/src/main/res/values/attrs.xml
index 8f6ed0ab246..de216bfa8b5 100644
--- a/components/feature/prompts/src/main/res/values/attrs.xml
+++ b/components/feature/prompts/src/main/res/values/attrs.xml
@@ -16,4 +16,8 @@
+
+
+
+
diff --git a/components/feature/prompts/src/main/res/values/strings.xml b/components/feature/prompts/src/main/res/values/strings.xml
index 8cbfa840cd3..42cf481ab1f 100644
--- a/components/feature/prompts/src/main/res/values/strings.xml
+++ b/components/feature/prompts/src/main/res/values/strings.xml
@@ -23,6 +23,8 @@
Don’t saveNever save
+
+ Not nowSave
@@ -106,6 +108,22 @@
Expand suggested credit cardsCollapse suggested credit cards
-
+
Manage credit cards
+
+ Securely save this card?
+
+ Update card expiration date?
+
+ Card number will be encrypted. Security code won’t be saved.
+
+
+
+ Select addresses
+
+ Expand suggested addresses
+
+ Collapse suggested addresses
+
+ Manage addresses
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt
index af42a910830..094e93eb286 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt
@@ -17,11 +17,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
@@ -31,7 +27,6 @@ import mozilla.components.browser.state.state.createCustomTab
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.Choice
-import mozilla.components.concept.engine.prompt.CreditCard
import mozilla.components.concept.engine.prompt.PromptRequest
import mozilla.components.concept.engine.prompt.PromptRequest.Alert
import mozilla.components.concept.engine.prompt.PromptRequest.Authentication
@@ -43,10 +38,15 @@ import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice
import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
import mozilla.components.concept.engine.prompt.PromptRequest.TextPrompt
import mozilla.components.concept.engine.prompt.ShareData
+import mozilla.components.concept.storage.Address
+import mozilla.components.concept.storage.CreditCardEntry
import mozilla.components.concept.storage.Login
import mozilla.components.concept.storage.LoginEntry
+import mozilla.components.feature.prompts.address.AddressDelegate
+import mozilla.components.feature.prompts.address.AddressPicker
import mozilla.components.feature.prompts.concept.SelectablePromptView
import mozilla.components.feature.prompts.creditcard.CreditCardPicker
+import mozilla.components.feature.prompts.creditcard.CreditCardSaveDialogFragment
import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment
import mozilla.components.feature.prompts.dialog.ConfirmDialogFragment
import mozilla.components.feature.prompts.dialog.MultiButtonDialogFragment
@@ -61,13 +61,14 @@ import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
import mozilla.components.support.test.whenever
-import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.`when`
@@ -84,12 +85,14 @@ import java.util.Date
@RunWith(AndroidJUnit4::class)
class PromptFeatureTest {
- private val testDispatcher = TestCoroutineDispatcher()
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
private lateinit var store: BrowserStore
private lateinit var fragmentManager: FragmentManager
private lateinit var loginPicker: LoginPicker
private lateinit var creditCardPicker: CreditCardPicker
+ private lateinit var addressPicker: AddressPicker
private val tabId = "test-tab"
private fun tab(): TabSessionState? {
@@ -99,7 +102,6 @@ class PromptFeatureTest {
@Before
@ExperimentalCoroutinesApi
fun setUp() {
- Dispatchers.setMain(testDispatcher)
store = BrowserStore(
BrowserState(
tabs = listOf(
@@ -113,16 +115,10 @@ class PromptFeatureTest {
)
loginPicker = mock()
creditCardPicker = mock()
+ addressPicker = mock()
fragmentManager = mockFragmentManager()
}
- @After
- @ExperimentalCoroutinesApi
- fun tearDown() {
- Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
- }
-
@Test
fun `PromptFeature acts on the selected session by default`() {
val feature = spy(
@@ -136,7 +132,6 @@ class PromptFeatureTest {
val promptRequest = SingleChoice(arrayOf(), {}, {})
store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
- testDispatcher.advanceUntilIdle()
verify(feature).onPromptRequested(store.state.tabs.first())
}
@@ -155,7 +150,6 @@ class PromptFeatureTest {
val promptRequest = SingleChoice(arrayOf(), {}, {})
store.dispatch(ContentAction.UpdatePromptRequestAction("custom-tab", promptRequest))
.joinBlocking()
- testDispatcher.advanceUntilIdle()
verify(feature).onPromptRequested(store.state.customTabs.first())
}
@@ -172,7 +166,6 @@ class PromptFeatureTest {
val promptRequest = SingleChoice(arrayOf(), {}, {})
store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest)).joinBlocking()
- testDispatcher.advanceUntilIdle()
feature.start()
verify(feature).onPromptRequested(store.state.tabs.first())
}
@@ -304,8 +297,13 @@ class PromptFeatureTest {
@Test
fun `GIVEN saveLoginPrompt is visible WHEN prompt is removed from state THEN dismiss saveLoginPrompt`() {
// given
+ val loginUsername = "username"
+ val loginPassword = "password"
+ val entry: LoginEntry = mock()
+ `when`(entry.username).thenReturn(loginUsername)
+ `when`(entry.password).thenReturn(loginPassword)
+ val promptRequest = PromptRequest.SaveLoginPrompt(2, listOf(entry), { }, { })
val saveLoginPrompt: SaveLoginDialogFragment = mock()
- val promptRequest: PromptRequest.SaveLoginPrompt = mock()
store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest))
.joinBlocking()
@@ -315,7 +313,9 @@ class PromptFeatureTest {
PromptFeature(
mock(),
store,
- fragmentManager = fragmentManager
+ fragmentManager = fragmentManager,
+ isSaveLoginEnabled = { true },
+ loginValidationDelegate = mock()
) { }
)
@@ -331,6 +331,96 @@ class PromptFeatureTest {
verify(saveLoginPrompt).dismissAllowingStateLoss()
}
+ @Test
+ fun `GIVEN isSaveLoginEnabled is false WHEN saveLoginPrompt request is handled THEN dismiss saveLoginPrompt`() {
+ val promptRequest = spy(
+ PromptRequest.SaveLoginPrompt(
+ hint = 2,
+ logins = emptyList(),
+ onConfirm = {},
+ onDismiss = {}
+ )
+ )
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fragmentManager = fragmentManager,
+ isSaveLoginEnabled = { false },
+ ) {}
+ )
+ val session = tab()!!
+
+ feature.handleDialogsRequest(promptRequest, session)
+
+ store.waitUntilIdle()
+
+ verify(feature).dismissDialogRequest(promptRequest, session)
+ }
+
+ @Test
+ fun `GIVEN loginValidationDelegate is null WHEN saveLoginPrompt request is handled THEN dismiss saveLoginPrompt`() {
+ val promptRequest = spy(
+ PromptRequest.SaveLoginPrompt(
+ hint = 2,
+ logins = emptyList(),
+ onConfirm = {},
+ onDismiss = {}
+ )
+ )
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fragmentManager = fragmentManager,
+ isSaveLoginEnabled = { true },
+ ) {}
+ )
+ val session = tab()!!
+
+ feature.handleDialogsRequest(promptRequest, session)
+
+ store.waitUntilIdle()
+
+ verify(feature).dismissDialogRequest(promptRequest, session)
+ }
+
+ @Test
+ fun `WHEN dismissDialogRequest is called THEN dismiss and consume the prompt request`() {
+ val tab = createTab("https://www.mozilla.org", id = tabId)
+ val store = spy(
+ BrowserStore(
+ BrowserState(
+ tabs = listOf(tab),
+ customTabs = listOf(
+ createCustomTab("https://www.mozilla.org", id = "custom-tab")
+ ),
+ selectedTabId = tabId
+ )
+ )
+ )
+ val feature = PromptFeature(
+ activity = mock(),
+ store = store,
+ fragmentManager = fragmentManager,
+ ) {}
+
+ var onDismissWasCalled = false
+ val promptRequest = PromptRequest.SaveLoginPrompt(
+ hint = 2,
+ logins = emptyList(),
+ onConfirm = {},
+ onDismiss = { onDismissWasCalled = true }
+ )
+
+ feature.dismissDialogRequest(promptRequest, tab)
+
+ store.waitUntilIdle()
+
+ verify(store).dispatch(ContentAction.ConsumePromptRequestAction(tab.id, promptRequest))
+ assertTrue(onDismissWasCalled)
+ }
+
@Test
fun `GIVEN loginPickerView is not visible WHEN dismissSelectPrompts THEN dismissCurrentLoginSelect called and false returned`() {
// given
@@ -410,7 +500,7 @@ class PromptFeatureTest {
@Test
fun `GIVEN creditCardPickerView is visible WHEN dismissSelectPrompts is called THEN dismissSelectCreditCardRequest returns true`() {
- val creditCardPickerView: SelectablePromptView = mock()
+ val creditCardPickerView: SelectablePromptView = mock()
val feature = spy(
PromptFeature(
mock(),
@@ -434,7 +524,7 @@ class PromptFeatureTest {
@Test
fun `GIVEN creditCardPickerView is not visible WHEN dismissSelectPrompts is called THEN dismissSelectPrompt returns false`() {
- val creditCardPickerView: SelectablePromptView = mock()
+ val creditCardPickerView: SelectablePromptView = mock()
val feature = spy(
PromptFeature(
mock(),
@@ -457,7 +547,7 @@ class PromptFeatureTest {
@Test
fun `GIVEN an active select credit card request WHEN onBackPressed is called THEN dismissSelectPrompts is called`() {
- val creditCardPickerView: SelectablePromptView = mock()
+ val creditCardPickerView: SelectablePromptView = mock()
val feature = spy(
PromptFeature(
mock(),
@@ -481,7 +571,7 @@ class PromptFeatureTest {
@Test
fun `WHEN dismissSelectPrompts is called THEN the active credit card picker should be dismissed`() {
- val creditCardPickerView: SelectablePromptView = mock()
+ val creditCardPickerView: SelectablePromptView = mock()
val feature = spy(
PromptFeature(
mock(),
@@ -507,6 +597,64 @@ class PromptFeatureTest {
verify(feature.creditCardPicker!!).dismissSelectCreditCardRequest(selectCreditCardRequest)
}
+ @Test
+ fun `WHEN dismissSelectPrompts is called THEN the active addressPicker dismiss should be called`() {
+ val addressPickerView: SelectablePromptView = mock()
+ val addressDelegate: AddressDelegate = mock()
+ val feature = spy(
+ PromptFeature(
+ mock(),
+ store,
+ fragmentManager = fragmentManager,
+ addressDelegate = addressDelegate
+ ) { }
+ )
+ feature.addressPicker = addressPicker
+ feature.activePromptRequest = mock()
+
+ whenever(addressDelegate.addressPickerView).thenReturn(addressPickerView)
+ whenever(addressPickerView.asView()).thenReturn(mock())
+ whenever(addressPickerView.asView().visibility).thenReturn(View.VISIBLE)
+
+ feature.dismissSelectPrompts()
+ verify(feature.addressPicker!!, never()).dismissSelectAddressRequest(any())
+
+ val selectAddressPromptRequest = mock()
+ feature.activePromptRequest = selectAddressPromptRequest
+
+ feature.dismissSelectPrompts()
+
+ verify(feature.addressPicker!!).dismissSelectAddressRequest(selectAddressPromptRequest)
+
+ store.waitUntilIdle()
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN addressPickerView is not visible WHEN dismissSelectPrompts is called THEN dismissSelectPrompts returns false`() {
+ val addressPickerView: SelectablePromptView = mock()
+ val addressDelegate: AddressDelegate = mock()
+ val feature = spy(
+ PromptFeature(
+ mock(),
+ store,
+ fragmentManager = fragmentManager,
+ addressDelegate = addressDelegate
+ ) { }
+ )
+ val selectAddressRequest = mock()
+ feature.addressPicker = addressPicker
+ feature.activePromptRequest = selectAddressRequest
+
+ whenever(addressDelegate.addressPickerView).thenReturn(addressPickerView)
+ whenever(addressPickerView.asView()).thenReturn(mock())
+ whenever(addressPickerView.asView().visibility).thenReturn(View.GONE)
+
+ val result = feature.dismissSelectPrompts()
+
+ assertEquals(false, result)
+ }
+
@Test
fun `Calling onCancel will consume promptRequest`() {
val feature =
@@ -817,7 +965,7 @@ class PromptFeatureTest {
@Test
fun `WHEN onActivityResult is called with PIN_REQUEST and RESULT_OK THEN onAuthSuccess) is called`() {
- val creditCardPickerView: SelectablePromptView = mock()
+ val creditCardPickerView: SelectablePromptView = mock()
val feature =
PromptFeature(
activity = mock(),
@@ -836,7 +984,7 @@ class PromptFeatureTest {
@Test
fun `WHEN onActivityResult is called with PIN_REQUEST and RESULT_CANCELED THEN onAuthFailure is called`() {
- val creditCardPickerView: SelectablePromptView = mock()
+ val creditCardPickerView: SelectablePromptView = mock()
val feature =
PromptFeature(
activity = mock(),
@@ -855,7 +1003,7 @@ class PromptFeatureTest {
@Test
fun `GIVEN user successfully authenticates by biometric prompt WHEN onBiometricResult is called THEN onAuthSuccess is called`() {
- val creditCardPickerView: SelectablePromptView = mock()
+ val creditCardPickerView: SelectablePromptView = mock()
val feature =
PromptFeature(
activity = mock(),
@@ -873,7 +1021,7 @@ class PromptFeatureTest {
@Test
fun `GIVEN user fails to authenticate by biometric prompt WHEN onBiometricResult is called THEN onAuthFailure) is called`() {
- val creditCardPickerView: SelectablePromptView = mock()
+ val creditCardPickerView: SelectablePromptView = mock()
val feature =
PromptFeature(
activity = mock(),
@@ -925,7 +1073,7 @@ class PromptFeatureTest {
@Test
fun `WHEN a credit card is selected THEN confirm the prompt request with the selected credit card`() {
- val creditCard = CreditCard(
+ val creditCard = CreditCardEntry(
guid = "id",
name = "Banana Apple",
number = "4111111111111110",
@@ -935,7 +1083,7 @@ class PromptFeatureTest {
)
var onDismissCalled = false
var onConfirmCalled = false
- var confirmedCreditCard: CreditCard? = null
+ var confirmedCreditCard: CreditCardEntry? = null
val selectCreditCardRequest = PromptRequest.SelectCreditCard(
creditCards = listOf(creditCard),
@@ -1393,7 +1541,7 @@ class PromptFeatureTest {
@Test
fun `WHEN page is refreshed THEN credit card prompt is dismissed`() {
- val creditCardPickerView: SelectablePromptView = mock()
+ val creditCardPickerView: SelectablePromptView = mock()
val feature =
PromptFeature(
activity = mock(),
@@ -1404,8 +1552,8 @@ class PromptFeatureTest {
) { }
feature.creditCardPicker = creditCardPicker
val onDismiss: () -> Unit = {}
- val onConfirm: (CreditCard) -> Unit = {}
- val creditCard = CreditCard(
+ val onConfirm: (CreditCardEntry) -> Unit = {}
+ val creditCard = CreditCardEntry(
guid = "1",
name = "Banana Apple",
number = "4111111111111110",
@@ -1449,7 +1597,6 @@ class PromptFeatureTest {
val promptRequest = PromptRequest.Share(ShareData("Title", "Text", null), {}, {}, {})
store.dispatch(ContentAction.UpdatePromptRequestAction("custom-tab", promptRequest))
.joinBlocking()
- testDispatcher.advanceUntilIdle()
verify(feature).onPromptRequested(store.state.customTabs.first())
verify(delegate).showShareSheet(
@@ -1477,7 +1624,6 @@ class PromptFeatureTest {
store.dispatch(ContentAction.UpdatePromptRequestAction("custom-tab", selectCreditCardRequest))
.joinBlocking()
- testDispatcher.advanceUntilIdle()
verify(feature).onPromptRequested(store.state.customTabs.first())
verify(creditCardPicker).handleSelectCreditCardRequest(selectCreditCardRequest)
@@ -1500,7 +1646,6 @@ class PromptFeatureTest {
store.dispatch(ContentAction.UpdatePromptRequestAction("custom-tab", selectCreditCardRequest))
.joinBlocking()
- testDispatcher.advanceUntilIdle()
verify(feature).onPromptRequested(store.state.customTabs.first())
verify(creditCardPicker, never()).handleSelectCreditCardRequest(selectCreditCardRequest)
@@ -1523,12 +1668,79 @@ class PromptFeatureTest {
store.dispatch(ContentAction.UpdatePromptRequestAction("custom-tab", selectCreditCardRequest))
.joinBlocking()
- testDispatcher.advanceUntilIdle()
verify(feature).onPromptRequested(store.state.customTabs.first())
verify(creditCardPicker, never()).handleSelectCreditCardRequest(selectCreditCardRequest)
}
+ @Test
+ fun `GIVEN isCreditCardAutofillEnabled is false WHEN SaveCreditCard request is handled THEN dismiss SaveCreditCard`() {
+ val creditCardEntry = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = ""
+ )
+ val promptRequest = spy(
+ PromptRequest.SaveCreditCard(
+ creditCard = creditCardEntry,
+ onConfirm = {},
+ onDismiss = {}
+ )
+ )
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fragmentManager = fragmentManager,
+ isCreditCardAutofillEnabled = { false },
+ ) {}
+ )
+ val session = tab()!!
+
+ feature.handleDialogsRequest(promptRequest, session)
+
+ store.waitUntilIdle()
+
+ verify(feature).dismissDialogRequest(promptRequest, session)
+ }
+
+ @Test
+ fun `GIVEN creditCardValidationDelegate is null WHEN SaveCreditCard request is handled THEN dismiss SaveCreditCard`() {
+ val creditCardEntry = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = ""
+ )
+ val promptRequest = spy(
+ PromptRequest.SaveCreditCard(
+ creditCard = creditCardEntry,
+ onConfirm = {},
+ onDismiss = {}
+ )
+ )
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fragmentManager = fragmentManager,
+ isCreditCardAutofillEnabled = { true },
+ ) {}
+ )
+ val session = tab()!!
+
+ feature.handleDialogsRequest(promptRequest, session)
+
+ store.waitUntilIdle()
+
+ verify(feature).dismissDialogRequest(promptRequest, session)
+ }
+
@Test
fun `Selecting an item in a share dialog will consume promptRequest`() {
val delegate: ShareDelegate = mock()
@@ -1879,6 +2091,204 @@ class PromptFeatureTest {
assertTrue(tab()!!.content.promptRequests.isEmpty())
}
+ @Test
+ fun `WHEN onConfirm is called on a SaveCreditCard dialog THEN a confirm request will consume the dialog`() {
+ val feature = PromptFeature(
+ activity = mock(),
+ store = store,
+ fragmentManager = fragmentManager,
+ isCreditCardAutofillEnabled = { true },
+ creditCardValidationDelegate = mock()
+ ) { }
+ val creditCardEntry = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = ""
+ )
+
+ val request = PromptRequest.SaveCreditCard(
+ creditCard = creditCardEntry,
+ onConfirm = {},
+ onDismiss = {}
+ )
+
+ feature.start()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, request)).joinBlocking()
+
+ assertEquals(1, tab()!!.content.promptRequests.size)
+
+ feature.onConfirm(
+ sessionId = tabId,
+ promptRequestUID = request.uid,
+ value = creditCardEntry
+ )
+
+ store.waitUntilIdle()
+
+ assertTrue(tab()!!.content.promptRequests.isEmpty())
+ }
+
+ @Test
+ fun `WHEN a credit card is confirmed to save THEN confirm the prompt request with the selected credit card`() {
+ val creditCardEntry = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = ""
+ )
+ var onDismissCalled = false
+ var onConfirmCalled = false
+ var confirmedCreditCard: CreditCardEntry? = null
+
+ val request = PromptRequest.SaveCreditCard(
+ creditCard = creditCardEntry,
+ onConfirm = {
+ confirmedCreditCard = it
+ onConfirmCalled = true
+ },
+ onDismiss = {
+ onDismissCalled = true
+ }
+ )
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, request)).joinBlocking()
+
+ request.onConfirm(creditCardEntry)
+
+ store.waitUntilIdle()
+
+ assertEquals(creditCardEntry, confirmedCreditCard)
+ assertTrue(onConfirmCalled)
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, request)).joinBlocking()
+
+ request.onDismiss()
+
+ store.waitUntilIdle()
+
+ assertTrue(onDismissCalled)
+ }
+
+ @Test
+ fun `WHEN the save credit card dialog fragment is created THEN the credit card entry is passed into the instance`() {
+ val feature = PromptFeature(
+ activity = mock(),
+ store = store,
+ fragmentManager = fragmentManager,
+ isCreditCardAutofillEnabled = { true },
+ creditCardValidationDelegate = mock()
+ ) { }
+ val creditCardEntry = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = ""
+ )
+ val request = PromptRequest.SaveCreditCard(
+ creditCard = creditCardEntry,
+ onConfirm = {},
+ onDismiss = {}
+ )
+
+ val contentState: ContentState = mock()
+ val session: TabSessionState = mock()
+ val sessionId = "sessionId"
+
+ `when`(session.content).thenReturn(contentState)
+ `when`(session.id).thenReturn(sessionId)
+
+ feature.handleDialogsRequest(
+ promptRequest = request,
+ session = session
+ )
+
+ assertTrue(feature.activePrompt!!.get() is CreditCardSaveDialogFragment)
+
+ val dialogFragment = feature.activePrompt!!.get() as CreditCardSaveDialogFragment
+
+ assertEquals(sessionId, dialogFragment.sessionId)
+ assertEquals(creditCardEntry, dialogFragment.creditCard)
+ }
+
+ @Test
+ fun `GIVEN SaveCreditCard prompt is shown WHEN prompt is removed from state THEN dismiss SaveCreditCard prompt`() {
+ val creditCardEntry = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = ""
+ )
+ val promptRequest = PromptRequest.SaveCreditCard(
+ creditCard = creditCardEntry,
+ onConfirm = {},
+ onDismiss = {}
+ )
+ val dialogFragment: CreditCardSaveDialogFragment = mock()
+
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, promptRequest))
+ .joinBlocking()
+ store.waitUntilIdle()
+
+ val feature = PromptFeature(
+ activity = mock(),
+ store = store,
+ fragmentManager = fragmentManager,
+ isCreditCardAutofillEnabled = { true },
+ creditCardValidationDelegate = mock()
+ ) { }
+
+ feature.start()
+ feature.activePrompt = WeakReference(dialogFragment)
+ feature.activePromptRequest = promptRequest
+
+ store.dispatch(ContentAction.ConsumePromptRequestAction(tabId, promptRequest))
+ .joinBlocking()
+
+ verify(dialogFragment).dismissAllowingStateLoss()
+ }
+
+ @Test
+ fun `WHEN promptRequest is updated THEN the replaced active prompt will be dismissed`() {
+ val feature = spy(
+ PromptFeature(
+ activity = mock(),
+ store = store,
+ fragmentManager = fragmentManager,
+ shareDelegate = mock()
+ ) { }
+ )
+ feature.start()
+
+ val previousPrompt = SingleChoice(
+ choices = arrayOf(),
+ onConfirm = {},
+ onDismiss = {}
+ )
+ val updatedPrompt = SingleChoice(
+ choices = arrayOf(),
+ onConfirm = {},
+ onDismiss = {}
+ )
+ store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, previousPrompt)).joinBlocking()
+
+ val fragment = mock()
+ whenever(fragment.shouldDismissOnLoad).thenReturn(true)
+ feature.activePrompt = WeakReference(fragment)
+
+ store.dispatch(ContentAction.ReplacePromptRequestAction(tabId, previousPrompt.uid, updatedPrompt)).joinBlocking()
+ verify(fragment).dismiss()
+ }
+
private fun mockFragmentManager(): FragmentManager {
val fragmentManager: FragmentManager = mock()
val transaction: FragmentTransaction = mock()
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptMiddlewareTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptMiddlewareTest.kt
index 649947918ac..b5cc25f2e64 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptMiddlewareTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptMiddlewareTest.kt
@@ -28,7 +28,6 @@ class PromptMiddlewareTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val testDispatcher = coroutinesTestRule.testDispatcher
private lateinit var store: BrowserStore
@@ -56,14 +55,12 @@ class PromptMiddlewareTest {
val onDeny = spy { }
val popupPrompt1 = PromptRequest.Popup("https://firefox.com", onAllow = { }, onDeny = onDeny)
store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, popupPrompt1)).joinBlocking()
- testDispatcher.advanceUntilIdle()
assertEquals(1, tab()!!.content.promptRequests.size)
assertEquals(popupPrompt1, tab()!!.content.promptRequests[0])
verify(onDeny, never()).invoke()
val popupPrompt2 = PromptRequest.Popup("https://firefox.com", onAllow = { }, onDeny = onDeny)
store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, popupPrompt2)).joinBlocking()
- testDispatcher.advanceUntilIdle()
assertEquals(1, tab()!!.content.promptRequests.size)
assertEquals(popupPrompt1, tab()!!.content.promptRequests[0])
verify(onDeny).invoke()
@@ -74,14 +71,12 @@ class PromptMiddlewareTest {
val onDeny = spy { }
val popupPrompt = PromptRequest.Popup("https://firefox.com", onAllow = { }, onDeny = onDeny)
store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, popupPrompt)).joinBlocking()
- testDispatcher.advanceUntilIdle()
assertEquals(1, tab()!!.content.promptRequests.size)
assertEquals(popupPrompt, tab()!!.content.promptRequests[0])
verify(onDeny, never()).invoke()
val alert = PromptRequest.Alert("title", "message", false, { }, { })
store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, alert)).joinBlocking()
- testDispatcher.advanceUntilIdle()
assertEquals(2, tab()!!.content.promptRequests.size)
assertEquals(popupPrompt, tab()!!.content.promptRequests[0])
assertEquals(alert, tab()!!.content.promptRequests[1])
@@ -91,14 +86,12 @@ class PromptMiddlewareTest {
fun `Process popup after other prompt request`() {
val alert = PromptRequest.Alert("title", "message", false, { }, { })
store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, alert)).joinBlocking()
- testDispatcher.advanceUntilIdle()
assertEquals(1, tab()!!.content.promptRequests.size)
assertEquals(alert, tab()!!.content.promptRequests[0])
val onDeny = spy { }
val popupPrompt = PromptRequest.Popup("https://firefox.com", onAllow = { }, onDeny = onDeny)
store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, popupPrompt)).joinBlocking()
- testDispatcher.advanceUntilIdle()
assertEquals(2, tab()!!.content.promptRequests.size)
assertEquals(alert, tab()!!.content.promptRequests[0])
assertEquals(popupPrompt, tab()!!.content.promptRequests[1])
@@ -109,13 +102,11 @@ class PromptMiddlewareTest {
fun `Process other prompt requests`() {
val alert = PromptRequest.Alert("title", "message", false, { }, { })
store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, alert)).joinBlocking()
- testDispatcher.advanceUntilIdle()
assertEquals(1, tab()!!.content.promptRequests.size)
assertEquals(alert, tab()!!.content.promptRequests[0])
val beforeUnloadPrompt = PromptRequest.BeforeUnload("title", onLeave = { }, onStay = { })
store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, beforeUnloadPrompt)).joinBlocking()
- testDispatcher.advanceUntilIdle()
assertEquals(2, tab()!!.content.promptRequests.size)
assertEquals(alert, tab()!!.content.promptRequests[0])
assertEquals(beforeUnloadPrompt, tab()!!.content.promptRequests[1])
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressAdapterTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressAdapterTest.kt
new file mode 100644
index 00000000000..d6d5ca2200b
--- /dev/null
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressAdapterTest.kt
@@ -0,0 +1,79 @@
+
+package mozilla.components.feature.prompts.address
+
+import android.view.LayoutInflater
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.storage.Address
+import mozilla.components.feature.prompts.R
+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
+
+@RunWith(AndroidJUnit4::class)
+class AddressAdapterTest {
+
+ private val address = Address(
+ guid = "1",
+ givenName = "Location",
+ additionalName = "Location",
+ familyName = "Location",
+ organization = "Mozilla",
+ streetAddress = "1230 Main st",
+ addressLevel3 = "Location3",
+ addressLevel2 = "Location2",
+ addressLevel1 = "Location1",
+ postalCode = "90237",
+ country = "USA",
+ tel = "00",
+ email = "email"
+ )
+
+ @Test
+ fun testAddressDiffCallback() {
+ val address2 = address.copy()
+
+ assertTrue(
+ AddressDiffCallback.areItemsTheSame(address, address2)
+ )
+ assertTrue(
+ AddressDiffCallback.areContentsTheSame(address, address2)
+ )
+
+ val address3 = address.copy(guid = "2")
+
+ assertFalse(
+ AddressDiffCallback.areItemsTheSame(address, address3)
+ )
+ assertFalse(
+ AddressDiffCallback.areItemsTheSame(address, address3)
+ )
+ }
+
+ @Test
+ fun `WHEN an address is bound to the adapter THEN set the address display name`() {
+ val view =
+ LayoutInflater.from(testContext).inflate(R.layout.mozac_feature_prompts_address_list_item, null)
+ val addressName: TextView = view.findViewById(R.id.address_name)
+
+ AddressViewHolder(view) {}.bind(address)
+
+ assertEquals(address.displayFormat(), addressName.text)
+ }
+
+ @Test
+ fun `WHEN an address item is clicked THEN call the onAddressSelected callback`() {
+ var addressSelected = false
+ val view =
+ LayoutInflater.from(testContext).inflate(R.layout.mozac_feature_prompts_address_list_item, null)
+ val onAddressSelect: (Address) -> Unit = { addressSelected = true }
+
+ AddressViewHolder(view, onAddressSelect).bind(address)
+ view.performClick()
+
+ assertTrue(addressSelected)
+ }
+}
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressPickerTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressPickerTest.kt
new file mode 100644
index 00000000000..59d3466157a
--- /dev/null
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressPickerTest.kt
@@ -0,0 +1,101 @@
+/* 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.feature.prompts.address
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.storage.Address
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class AddressPickerTest {
+
+ private lateinit var store: BrowserStore
+ private lateinit var state: BrowserState
+ private lateinit var addressPicker: AddressPicker
+ private lateinit var addressSelectBar: AddressSelectBar
+
+ private val address = Address(
+ guid = "1",
+ givenName = "Location",
+ additionalName = "Location",
+ familyName = "Location",
+ organization = "Mozilla",
+ streetAddress = "1230 Main st",
+ addressLevel3 = "Location3",
+ addressLevel2 = "Location2",
+ addressLevel1 = "Location1",
+ postalCode = "90237",
+ country = "USA",
+ tel = "00",
+ email = "email"
+ )
+
+ private var onDismissCalled = false
+ private var confirmedAddress: Address? = null
+
+ private val promptRequest = PromptRequest.SelectAddress(
+ addresses = listOf(address),
+ onDismiss = { onDismissCalled = true },
+ onConfirm = { confirmedAddress = it }
+ )
+
+ @Before
+ fun setup() {
+ store = mock()
+ state = mock()
+ addressSelectBar = mock()
+ addressPicker = AddressPicker(
+ store = store,
+ addressSelectBar = addressSelectBar
+ )
+
+ whenever(store.state).thenReturn(state)
+ }
+
+ @Test
+ fun `WHEN onOptionSelect is called with an address THEN selectAddressCallback is invoked and prompt is hidden`() {
+ val content: ContentState = mock()
+ whenever(content.promptRequests).thenReturn(listOf(promptRequest))
+ val selectedTab = TabSessionState("browser-tab", content, mock(), mock())
+ whenever(state.selectedTabId).thenReturn(selectedTab.id)
+ whenever(state.tabs).thenReturn(listOf(selectedTab))
+
+ addressPicker.onOptionSelect(address)
+
+ verify(addressSelectBar).hidePrompt()
+ assertEquals(address, confirmedAddress)
+ }
+
+ @Test
+ fun `GIVEN a prompt request WHEN handleSelectAddressRequest is called THEN the prompt is shown with the provided addresses`() {
+ addressPicker.handleSelectAddressRequest(promptRequest)
+
+ verify(addressSelectBar).showPrompt(promptRequest.addresses)
+ }
+
+ @Test
+ fun `GIVEN a custom tab and a prompt request WHEN handleSelectAddressRequest is called THEN the prompt is shown with the provided addresses`() {
+ val customTabContent: ContentState = mock()
+ val customTab = CustomTabSessionState("custom-tab", customTabContent, mock(), mock())
+ whenever(customTabContent.promptRequests).thenReturn(listOf(promptRequest))
+ whenever(state.customTabs).thenReturn(listOf(customTab))
+
+ addressPicker.handleSelectAddressRequest(promptRequest)
+
+ verify(addressSelectBar).showPrompt(promptRequest.addresses)
+ }
+}
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressSelectBarTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressSelectBarTest.kt
new file mode 100644
index 00000000000..1d7751e2e78
--- /dev/null
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/address/AddressSelectBarTest.kt
@@ -0,0 +1,99 @@
+/* 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.feature.prompts.address
+
+import android.widget.LinearLayout
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.storage.Address
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.concept.SelectablePromptView
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class AddressSelectBarTest {
+
+ private lateinit var addressSelectBar: AddressSelectBar
+
+ private val address = Address(
+ guid = "1",
+ givenName = "Location",
+ additionalName = "Location",
+ familyName = "Location",
+ organization = "Mozilla",
+ streetAddress = "1230 Main st",
+ addressLevel3 = "Location3",
+ addressLevel2 = "Location2",
+ addressLevel1 = "Location1",
+ postalCode = "90237",
+ country = "USA",
+ tel = "00",
+ email = "email"
+ )
+
+ @Before
+ fun setup() {
+ addressSelectBar = AddressSelectBar(appCompatContext)
+ }
+
+ @Test
+ fun `WHEN showPrompt is called THEN the select bar is shown`() {
+ val addresses = listOf(address)
+
+ addressSelectBar.showPrompt(addresses)
+
+ assertTrue(addressSelectBar.isVisible)
+ }
+
+ @Test
+ fun `WHEN hidePrompt is called THEN the select bar is hidden`() {
+ assertTrue(addressSelectBar.isVisible)
+
+ addressSelectBar.hidePrompt()
+
+ assertFalse(addressSelectBar.isVisible)
+ }
+
+ @Test
+ fun `WHEN the selectBar header is clicked two times THEN the list of addresses is shown, then hidden`() {
+ addressSelectBar.showPrompt(listOf(address))
+ addressSelectBar.findViewById(R.id.select_address_header).performClick()
+
+ assertTrue(addressSelectBar.findViewById(R.id.address_list).isVisible)
+
+ addressSelectBar.findViewById(R.id.select_address_header).performClick()
+
+ assertFalse(addressSelectBar.findViewById(R.id.address_list).isVisible)
+ }
+
+ @Test
+ fun `GIVEN a listener WHEN an address is clicked THEN onOptionSelected is called`() {
+ val listener: SelectablePromptView.Listener = mock()
+
+ assertNull(addressSelectBar.listener)
+
+ addressSelectBar.listener = listener
+
+ addressSelectBar.showPrompt(listOf(address))
+ val adapter = addressSelectBar.findViewById(R.id.address_list).adapter as AddressAdapter
+ val holder = adapter.onCreateViewHolder(LinearLayout(testContext), 0)
+ adapter.bindViewHolder(holder, 0)
+
+ holder.itemView.performClick()
+
+ verify(listener).onOptionSelect(address)
+ }
+}
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolderTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolderTest.kt
index 0cb08d848e0..aecf8fe4775 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolderTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolderTest.kt
@@ -9,7 +9,7 @@ import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.test.ext.junit.runners.AndroidJUnit4
-import mozilla.components.concept.engine.prompt.CreditCard
+import mozilla.components.concept.storage.CreditCardEntry
import mozilla.components.feature.prompts.R
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
@@ -27,9 +27,9 @@ class CreditCardItemViewHolderTest {
private lateinit var cardLogoView: ImageView
private lateinit var cardNumberView: TextView
private lateinit var expirationDateView: TextView
- private lateinit var onCreditCardSelected: (CreditCard) -> Unit
+ private lateinit var onCreditCardSelected: (CreditCardEntry) -> Unit
- private val creditCard = CreditCard(
+ private val creditCard = CreditCardEntry(
guid = "1",
name = "Banana Apple",
number = "4111111111111111",
@@ -58,8 +58,8 @@ class CreditCardItemViewHolderTest {
@Test
fun `GIVEN a credit card item WHEN a credit item is clicked THEN onCreditCardSelected is called with the given credit card item`() {
- var onCreditCardSelectedCalled: CreditCard? = null
- val onCreditCardSelected = { creditCard: CreditCard ->
+ var onCreditCardSelectedCalled: CreditCardEntry? = null
+ val onCreditCardSelected = { creditCard: CreditCardEntry ->
onCreditCardSelectedCalled = creditCard
}
CreditCardItemViewHolder(view, onCreditCardSelected).bind(creditCard)
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardPickerTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardPickerTest.kt
index 3f7d07e4741..242661c0bbb 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardPickerTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardPickerTest.kt
@@ -10,8 +10,8 @@ import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
-import mozilla.components.concept.engine.prompt.CreditCard
import mozilla.components.concept.engine.prompt.PromptRequest
+import mozilla.components.concept.storage.CreditCardEntry
import mozilla.components.support.test.mock
import mozilla.components.support.test.whenever
import org.junit.Assert.assertEquals
@@ -30,7 +30,7 @@ class CreditCardPickerTest {
private lateinit var creditCardPicker: CreditCardPicker
private lateinit var creditCardSelectBar: CreditCardSelectBar
- private val creditCard = CreditCard(
+ private val creditCard = CreditCardEntry(
guid = "1",
name = "Banana Apple",
number = "4111111111111110",
@@ -39,7 +39,7 @@ class CreditCardPickerTest {
cardType = ""
)
var onDismissCalled = false
- var confirmedCreditCard: CreditCard? = null
+ var confirmedCreditCard: CreditCardEntry? = null
private val promptRequest = PromptRequest.SelectCreditCard(
creditCards = listOf(creditCard),
onDismiss = { onDismissCalled = true },
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSaveDialogFragmentTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSaveDialogFragmentTest.kt
new file mode 100644
index 00000000000..2a6b7489239
--- /dev/null
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSaveDialogFragmentTest.kt
@@ -0,0 +1,315 @@
+/* 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.feature.prompts.creditcard
+
+import android.widget.Button
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.view.isVisible
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.CreditCardValidationDelegate
+import mozilla.components.feature.prompts.PromptFeature
+import mozilla.components.feature.prompts.R
+import mozilla.components.feature.prompts.facts.CreditCardAutofillDialogFacts
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.processor.CollectionProcessor
+import mozilla.components.support.test.any
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class CreditCardSaveDialogFragmentTest {
+
+ private val creditCard = CreditCardEntry(
+ guid = "1",
+ name = "Banana Apple",
+ number = "4111111111111110",
+ expiryMonth = "5",
+ expiryYear = "2030",
+ cardType = "amex"
+ )
+ private val sessionId = "sessionId"
+ private val promptRequestUID = "uid"
+
+ @Test
+ fun `WHEN the credit card save dialog fragment view is created THEN the credit card entry is displayed`() {
+ val fragment = spy(
+ CreditCardSaveDialogFragment.newInstance(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ shouldDismissOnLoad = true,
+ creditCard = creditCard
+ )
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doAnswer {
+ FrameLayout(appCompatContext).apply {
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_header
+ }
+ )
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_message
+ }
+ )
+ addView(Button(appCompatContext).apply { id = R.id.save_confirm })
+ addView(Button(appCompatContext).apply { id = R.id.save_cancel })
+ addView(ImageView(appCompatContext).apply { id = R.id.credit_card_logo })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_number })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_expiration_date })
+ }
+ }.`when`(fragment).onCreateView(any(), any(), any())
+
+ val view = fragment.onCreateView(mock(), mock(), mock())
+ fragment.onViewCreated(view, mock())
+
+ val cardNumberTextView = view.findViewById(R.id.credit_card_number)
+ val iconImageView = view.findViewById(R.id.credit_card_logo)
+ val expiryDateView = view.findViewById(R.id.credit_card_expiration_date)
+
+ assertEquals(creditCard.obfuscatedCardNumber, cardNumberTextView.text)
+ assertEquals(creditCard.expiryDate, expiryDateView.text)
+ assertNotNull(iconImageView.drawable)
+ }
+
+ @Test
+ fun `WHEN setViewText is called with new header and button text THEN the header and button text are updated in the view`() {
+ val fragment = spy(
+ CreditCardSaveDialogFragment.newInstance(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ shouldDismissOnLoad = true,
+ creditCard = creditCard
+ )
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doAnswer {
+ FrameLayout(appCompatContext).apply {
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_header
+ }
+ )
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_message
+ }
+ )
+ addView(Button(appCompatContext).apply { id = R.id.save_confirm })
+ addView(Button(appCompatContext).apply { id = R.id.save_cancel })
+ addView(ImageView(appCompatContext).apply { id = R.id.credit_card_logo })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_number })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_expiration_date })
+ }
+ }.`when`(fragment).onCreateView(any(), any(), any())
+
+ val view = fragment.onCreateView(mock(), mock(), mock())
+ fragment.onViewCreated(view, mock())
+
+ val headerTextView = view.findViewById(R.id.save_credit_card_header)
+ val messageTextView = view.findViewById(R.id.save_credit_card_message)
+ val cancelButtonView = view.findViewById(R.id.save_cancel)
+ val confirmButtonView = view.findViewById(R.id.save_confirm)
+
+ val header = "header"
+ val cancelButtonText = "cancelButtonText"
+ val confirmButtonText = "confirmButtonText"
+
+ fragment.setViewText(
+ view = view,
+ header = header,
+ cancelButtonText = cancelButtonText,
+ confirmButtonText = confirmButtonText,
+ showMessageBody = false
+ )
+
+ assertEquals(header, headerTextView.text)
+ assertEquals(cancelButtonText, cancelButtonView.text)
+ assertEquals(confirmButtonText, confirmButtonView.text)
+ assertFalse(messageTextView.isVisible)
+
+ fragment.setViewText(
+ view = view,
+ header = header,
+ cancelButtonText = cancelButtonText,
+ confirmButtonText = confirmButtonText,
+ showMessageBody = true
+ )
+
+ assertTrue(messageTextView.isVisible)
+ }
+
+ @Test
+ fun `WHEN the confirm button is clicked THEN the prompt feature is notified`() {
+ val mockFeature: PromptFeature = mock()
+ val fragment = spy(
+ CreditCardSaveDialogFragment.newInstance(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ shouldDismissOnLoad = true,
+ creditCard = creditCard
+ )
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doAnswer {
+ FrameLayout(appCompatContext).apply {
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_header
+ }
+ )
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_message
+ }
+ )
+ addView(Button(appCompatContext).apply { id = R.id.save_confirm })
+ addView(Button(appCompatContext).apply { id = R.id.save_cancel })
+ addView(ImageView(appCompatContext).apply { id = R.id.credit_card_logo })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_number })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_expiration_date })
+ }
+ }.`when`(fragment).onCreateView(any(), any(), any())
+ doNothing().`when`(fragment).dismiss()
+
+ val view = fragment.onCreateView(mock(), mock(), mock())
+ fragment.onViewCreated(view, mock())
+
+ val buttonView = view.findViewById(R.id.save_confirm)
+
+ buttonView.performClick()
+
+ verify(mockFeature).onConfirm(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ value = creditCard
+ )
+ }
+
+ @Test
+ fun `WHEN the cancel button is clicked THEN the prompt feature is notified`() {
+ val mockFeature: PromptFeature = mock()
+ val fragment = spy(
+ CreditCardSaveDialogFragment.newInstance(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ shouldDismissOnLoad = true,
+ creditCard = creditCard
+ )
+ )
+
+ fragment.feature = mockFeature
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doAnswer {
+ FrameLayout(appCompatContext).apply {
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_header
+ }
+ )
+ addView(
+ AppCompatTextView(appCompatContext).apply {
+ id = R.id.save_credit_card_message
+ }
+ )
+ addView(Button(appCompatContext).apply { id = R.id.save_confirm })
+ addView(Button(appCompatContext).apply { id = R.id.save_cancel })
+ addView(ImageView(appCompatContext).apply { id = R.id.credit_card_logo })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_number })
+ addView(TextView(appCompatContext).apply { id = R.id.credit_card_expiration_date })
+ }
+ }.`when`(fragment).onCreateView(any(), any(), any())
+ doNothing().`when`(fragment).dismiss()
+
+ val view = fragment.onCreateView(mock(), mock(), mock())
+ fragment.onViewCreated(view, mock())
+
+ val buttonView = view.findViewById(R.id.save_cancel)
+
+ buttonView.performClick()
+
+ verify(mockFeature).onCancel(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID
+ )
+ }
+
+ @Test
+ fun `WHEN the confirm save button is clicked THEN the appropriate fact is emitted`() {
+ val fragment = spy(
+ CreditCardSaveDialogFragment.newInstance(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ shouldDismissOnLoad = true,
+ creditCard = creditCard
+ )
+ )
+
+ fragment.confirmResult = CreditCardValidationDelegate.Result.CanBeCreated
+
+ CollectionProcessor.withFactCollection { facts ->
+ fragment.emitSaveUpdateFact()
+
+ assertEquals(1, facts.size)
+ val fact = facts.single()
+ assertEquals(Component.FEATURE_PROMPTS, fact.component)
+ assertEquals(Action.CONFIRM, fact.action)
+ assertEquals(
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_CREATED,
+ fact.item
+ )
+ }
+ }
+
+ @Test
+ fun `WHEN the confirm update button is clicked THEN the appropriate fact is emitted`() {
+ val fragment = spy(
+ CreditCardSaveDialogFragment.newInstance(
+ sessionId = sessionId,
+ promptRequestUID = promptRequestUID,
+ shouldDismissOnLoad = true,
+ creditCard = creditCard
+ )
+ )
+
+ fragment.confirmResult = CreditCardValidationDelegate.Result.CanBeUpdated(mock())
+
+ CollectionProcessor.withFactCollection { facts ->
+ fragment.emitSaveUpdateFact()
+
+ assertEquals(1, facts.size)
+ val fact = facts.single()
+ assertEquals(Component.FEATURE_PROMPTS, fact.component)
+ assertEquals(Action.CONFIRM, fact.action)
+ assertEquals(
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_UPDATED,
+ fact.item
+ )
+ }
+ }
+}
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBarTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBarTest.kt
index deb09d182cc..df004e822ec 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBarTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBarTest.kt
@@ -9,8 +9,8 @@ import androidx.appcompat.widget.AppCompatTextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
-import mozilla.components.concept.engine.prompt.CreditCard
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.storage.CreditCardEntry
import mozilla.components.feature.prompts.R
import mozilla.components.feature.prompts.concept.SelectablePromptView
import mozilla.components.feature.prompts.facts.CreditCardAutofillDialogFacts
@@ -36,7 +36,7 @@ class CreditCardSelectBarTest {
private lateinit var creditCardSelectBar: CreditCardSelectBar
- private val creditCard = CreditCard(
+ private val creditCard = CreditCardEntry(
guid = "1",
name = "Banana Apple",
number = "4111111111111110",
@@ -68,7 +68,7 @@ class CreditCardSelectBarTest {
@Test
fun `GIVEN a listener WHEN manage credit cards button is clicked THEN onManageOptions is called`() {
- val listener: SelectablePromptView.Listener = mock()
+ val listener: SelectablePromptView.Listener = mock()
assertNull(creditCardSelectBar.listener)
@@ -81,8 +81,8 @@ class CreditCardSelectBarTest {
}
@Test
- fun `GIVEN a listener WHEN a credit card is selected THEN onOptionSelect is called`() = runBlocking {
- val listener: SelectablePromptView.Listener = mock()
+ fun `GIVEN a listener WHEN a credit card is selected THEN onOptionSelect is called`() = runTest {
+ val listener: SelectablePromptView.Listener = mock()
creditCardSelectBar.listener = listener
val facts = mutableListOf()
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapterTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapterTest.kt
index 414a0ec3fa1..6fbfc06a23e 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapterTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapterTest.kt
@@ -1,7 +1,7 @@
package mozilla.components.feature.prompts.creditcard
-import mozilla.components.concept.engine.prompt.CreditCard
+import mozilla.components.concept.storage.CreditCardEntry
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -10,7 +10,7 @@ class CreditCardsAdapterTest {
@Test
fun testDiffCallback() {
- val creditCard1 = CreditCard(
+ val creditCard1 = CreditCardEntry(
guid = "1",
name = "Banana Apple",
number = "4111111111111110",
@@ -27,7 +27,7 @@ class CreditCardsAdapterTest {
CreditCardsAdapter.DiffCallback.areContentsTheSame(creditCard1, creditCard2)
)
- val creditCard3 = CreditCard(
+ val creditCard3 = CreditCardEntry(
guid = "2",
name = "Pineapple Orange",
number = "4111111111115555",
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragmentTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragmentTest.kt
index 53f8f2d70a0..4dc9970e7f6 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragmentTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragmentTest.kt
@@ -481,6 +481,192 @@ class ChoiceDialogFragmentTest {
}
}
+ @Test
+ fun `disabled single choice item is not clickable`() {
+ val choices = arrayOf(
+ Choice(id = "item1", label = "Enabled choice"),
+ Choice(id = "item2", enable = false, label = "Disabled choice")
+ )
+
+ val fragment =
+ spy(newInstance(choices, "sessionId", "uid", false, SINGLE_CHOICE_DIALOG_TYPE))
+ fragment.feature = mockFeature
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = dialog.findViewById(R.id.recyclerView).adapter as ChoiceAdapter
+
+ // test disabled item
+ val disabledItemViewHolder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), adapter.getItemViewType(1))
+ as SingleViewHolder
+
+ adapter.bindViewHolder(disabledItemViewHolder, 1)
+
+ with(disabledItemViewHolder) {
+ assertEquals(labelView.text, "Disabled choice")
+ assertFalse(labelView.isEnabled)
+ assertFalse(itemView.isClickable)
+ }
+ }
+
+ @Test
+ fun `enabled single choice item is clickable`() {
+ val choices = arrayOf(
+ Choice(id = "item1", label = "Enabled choice"),
+ Choice(id = "item2", enable = false, label = "Disabled choice")
+ )
+
+ val fragment = spy(newInstance(choices, "sessionId", "uid", false, SINGLE_CHOICE_DIALOG_TYPE))
+ fragment.feature = mockFeature
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = dialog.findViewById(R.id.recyclerView).adapter as ChoiceAdapter
+
+ // test enabled item
+ val enabledItemViewHolder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), adapter.getItemViewType(0)) as SingleViewHolder
+
+ adapter.bindViewHolder(enabledItemViewHolder, 0)
+
+ with(enabledItemViewHolder) {
+ assertEquals(labelView.text, "Enabled choice")
+ assertTrue(labelView.isEnabled)
+ assertTrue(itemView.isClickable)
+ }
+ }
+
+ @Test
+ fun `disabled multiple choice item is not clickable`() {
+ val choices = arrayOf(
+ Choice(id = "item1", label = "Enabled choice"),
+ Choice(id = "item2", enable = false, label = "Disabled choice")
+ )
+
+ val fragment =
+ spy(newInstance(choices, "sessionId", "uid", false, MULTIPLE_CHOICE_DIALOG_TYPE))
+ fragment.feature = mockFeature
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = dialog.findViewById(R.id.recyclerView).adapter as ChoiceAdapter
+
+ // test disabled item
+ val disabledItemViewHolder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), adapter.getItemViewType(1))
+ as MultipleViewHolder
+
+ adapter.bindViewHolder(disabledItemViewHolder, 1)
+
+ with(disabledItemViewHolder) {
+ assertEquals(labelView.text, "Disabled choice")
+ assertFalse(labelView.isEnabled)
+ assertFalse(itemView.isClickable)
+ }
+ }
+
+ @Test
+ fun `enabled multiple choice item is clickable`() {
+ val choices = arrayOf(
+ Choice(id = "item1", label = "Enabled choice"),
+ Choice(id = "item2", enable = false, label = "Disabled choice")
+ )
+
+ val fragment = spy(newInstance(choices, "sessionId", "uid", false, MULTIPLE_CHOICE_DIALOG_TYPE))
+ fragment.feature = mockFeature
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = dialog.findViewById(R.id.recyclerView).adapter as ChoiceAdapter
+
+ // test enabled item
+ val enabledItemViewHolder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), adapter.getItemViewType(0)) as MultipleViewHolder
+
+ adapter.bindViewHolder(enabledItemViewHolder, 0)
+
+ with(enabledItemViewHolder) {
+ assertEquals(labelView.text, "Enabled choice")
+ assertTrue(labelView.isEnabled)
+ assertTrue(itemView.isClickable)
+ }
+ }
+
+ @Test
+ fun `disabled menu choice item is not clickable`() {
+ val choices = arrayOf(
+ Choice(id = "item1", label = "Enabled choice"),
+ Choice(id = "item2", enable = false, label = "Disabled choice")
+ )
+
+ val fragment =
+ spy(newInstance(choices, "sessionId", "uid", false, MENU_CHOICE_DIALOG_TYPE))
+ fragment.feature = mockFeature
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = dialog.findViewById(R.id.recyclerView).adapter as ChoiceAdapter
+
+ // test disabled item
+ val disabledItemViewHolder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), adapter.getItemViewType(1))
+ as MenuViewHolder
+
+ adapter.bindViewHolder(disabledItemViewHolder, 1)
+
+ with(disabledItemViewHolder) {
+ assertEquals(labelView.text, "Disabled choice")
+ assertFalse(labelView.isEnabled)
+ assertFalse(itemView.isClickable)
+ }
+ }
+
+ @Test
+ fun `enabled menu choice item is clickable`() {
+ val choices = arrayOf(
+ Choice(id = "item1", label = "Enabled choice"),
+ Choice(id = "item2", enable = false, label = "Disabled choice")
+ )
+
+ val fragment = spy(newInstance(choices, "sessionId", "uid", false, MENU_CHOICE_DIALOG_TYPE))
+ fragment.feature = mockFeature
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+ doNothing().`when`(fragment).dismiss()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val adapter = dialog.findViewById(R.id.recyclerView).adapter as ChoiceAdapter
+
+ // test enabled item
+ val enabledItemViewHolder =
+ adapter.onCreateViewHolder(LinearLayout(testContext), adapter.getItemViewType(0)) as MenuViewHolder
+
+ adapter.bindViewHolder(enabledItemViewHolder, 0)
+
+ with(enabledItemViewHolder) {
+ assertEquals(labelView.text, "Enabled choice")
+ assertTrue(labelView.isEnabled)
+ assertTrue(itemView.isClickable)
+ }
+ }
+
@Test
fun `scroll to selected item`() {
// array of 20 choices; 10th one is selected
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragmentTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragmentTest.kt
index 32a6f9c9ad2..ea11ebee102 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragmentTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragmentTest.kt
@@ -9,7 +9,6 @@ import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.DialogInterface.BUTTON_NEUTRAL
import android.content.DialogInterface.BUTTON_POSITIVE
-import android.os.Build.VERSION_CODES.LOLLIPOP
import android.os.Looper.getMainLooper
import android.widget.DatePicker
import android.widget.NumberPicker
@@ -39,7 +38,6 @@ import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations.openMocks
import org.robolectric.Shadows.shadowOf
-import org.robolectric.annotation.Config
import java.util.Calendar
import java.util.Date
@@ -211,8 +209,6 @@ class TimePickerDialogFragmentTest {
}
@Test
- @Config(sdk = [LOLLIPOP])
- @Suppress("DEPRECATION")
fun `building a time picker`() {
val initialDate = "2018-06-12T19:30".toDate("yyyy-MM-dd'T'HH:mm")
val minDate = "2018-06-07T00:00".toDate("yyyy-MM-dd'T'HH:mm")
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/facts/CreditCardAutofillDialogFactsTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/facts/CreditCardAutofillDialogFactsTest.kt
index 0660b8c7e5e..a138651ae4c 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/facts/CreditCardAutofillDialogFactsTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/facts/CreditCardAutofillDialogFactsTest.kt
@@ -91,4 +91,40 @@ class CreditCardAutofillDialogFactsTest {
}
}
}
+
+ @Test
+ fun `Emits facts for autofill confirm and create events`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitCreditCardAutofillCreatedFact()
+
+ assertEquals(1, facts.size)
+
+ val fact = facts.single()
+ assertEquals(Component.FEATURE_PROMPTS, fact.component)
+ assertEquals(Action.CONFIRM, fact.action)
+ assertEquals(
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_CREATED,
+ fact.item
+ )
+ }
+ }
+
+ @Test
+ fun `Emits facts for autofill confirm and update events`() {
+ CollectionProcessor.withFactCollection { facts ->
+
+ emitCreditCardAutofillUpdatedFact()
+
+ assertEquals(1, facts.size)
+
+ val fact = facts.single()
+ assertEquals(Component.FEATURE_PROMPTS, fact.component)
+ assertEquals(Action.CONFIRM, fact.action)
+ assertEquals(
+ CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_UPDATED,
+ fact.item
+ )
+ }
+ }
}
diff --git a/components/feature/push/src/test/java/mozilla/components/feature/push/AutoPushFeatureKtTest.kt b/components/feature/push/src/test/java/mozilla/components/feature/push/AutoPushFeatureKtTest.kt
index a8d8a72409a..6615bb4fd3d 100644
--- a/components/feature/push/src/test/java/mozilla/components/feature/push/AutoPushFeatureKtTest.kt
+++ b/components/feature/push/src/test/java/mozilla/components/feature/push/AutoPushFeatureKtTest.kt
@@ -4,11 +4,11 @@
package mozilla.components.feature.push
-import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
-import kotlinx.coroutines.plus
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.appservices.push.PushException.CommunicationException
import mozilla.appservices.push.PushException.CommunicationServerException
import mozilla.appservices.push.PushException.CryptoException
@@ -32,28 +32,29 @@ class AutoPushFeatureKtTest {
assertEquals(ServiceType.FCM, config.serviceType)
}
+ @OptIn(DelicateCoroutinesApi::class)
@Test
- fun `exception handler handles exceptions`() = runBlockingTest {
+ fun `exception handler handles exceptions`() = runTest {
var invoked = false
- val scope = CoroutineScope(coroutineContext) + exceptionHandler { invoked = true }
+ val handler = exceptionHandler { invoked = true }
- scope.launch { throw PushError.MalformedMessage("test") }
+ GlobalScope.launch(handler) { throw PushError.MalformedMessage("test") }.join()
assertFalse(invoked)
- scope.launch { throw GeneralException("test") }
+ GlobalScope.launch(handler) { throw GeneralException("test") }.join()
assertFalse(invoked)
- scope.launch { throw CryptoException("test") }
+ GlobalScope.launch(handler) { throw CryptoException("test") }.join()
assertFalse(invoked)
- scope.launch { throw CommunicationException("test") }
+ GlobalScope.launch(handler) { throw CommunicationException("test") }.join()
assertFalse(invoked)
- scope.launch { throw CommunicationServerException("test") }
+ GlobalScope.launch(handler) { throw CommunicationServerException("test") }.join()
assertFalse(invoked)
// An exception where we should invoke our callback.
- scope.launch { throw MissingRegistrationTokenException("") }
+ GlobalScope.launch(handler) { throw MissingRegistrationTokenException("") }.join()
assertTrue(invoked)
}
}
diff --git a/components/feature/push/src/test/java/mozilla/components/feature/push/AutoPushFeatureTest.kt b/components/feature/push/src/test/java/mozilla/components/feature/push/AutoPushFeatureTest.kt
index d8a3c4c156e..9d171273921 100644
--- a/components/feature/push/src/test/java/mozilla/components/feature/push/AutoPushFeatureTest.kt
+++ b/components/feature/push/src/test/java/mozilla/components/feature/push/AutoPushFeatureTest.kt
@@ -10,7 +10,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
import mozilla.appservices.push.PushException.GeneralException
import mozilla.appservices.push.PushException.MissingRegistrationTokenException
import mozilla.components.concept.base.crash.CrashReporting
@@ -25,6 +24,8 @@ import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import mozilla.components.support.test.nullable
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import mozilla.components.support.test.whenever
import org.junit.After
import org.junit.Assert.assertEquals
@@ -33,13 +34,12 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyString
-import org.mockito.ArgumentMatchers.nullable
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
-import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
@@ -47,6 +47,9 @@ import org.mockito.Mockito.verifyNoMoreInteractions
@RunWith(AndroidJUnit4::class)
class AutoPushFeatureTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
private var lastVerified: Long
get() = preference(testContext).getLong(LAST_VERIFIED, System.currentTimeMillis())
set(value) = preference(testContext).edit().putLong(LAST_VERIFIED, value).apply()
@@ -77,14 +80,14 @@ class AutoPushFeatureTest {
}
@Test
- fun `updateToken not called if no token in prefs`() = runBlockingTest {
+ fun `updateToken not called if no token in prefs`() = runTestOnMain {
AutoPushFeature(testContext, mock(), mock(), coroutineContext, connection)
verify(connection, never()).updateToken(anyString())
}
@Test
- fun `updateToken called if token is in prefs`() = runBlockingTest {
+ fun `updateToken called if token is in prefs`() = runTestOnMain {
preference(testContext).edit().putString(PREF_TOKEN, "token").apply()
val feature = AutoPushFeature(
@@ -97,7 +100,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `shutdown stops service and unsubscribes all`() = runBlockingTest {
+ fun `shutdown stops service and unsubscribes all`() = runTestOnMain {
val service: PushService = mock()
whenever(connection.isInitialized()).thenReturn(true)
@@ -109,7 +112,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `onNewToken updates connection and saves pref`() = runBlockingTest {
+ fun `onNewToken updates connection and saves pref`() = runTestOnMain {
val feature = AutoPushFeature(testContext, mock(), mock(), coroutineContext, connection)
whenever(connection.subscribe(anyString(), nullable())).thenReturn(mock())
@@ -124,7 +127,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `onMessageReceived decrypts message and notifies observers`() = runBlockingTest {
+ fun `onMessageReceived decrypts message and notifies observers`() = runTestOnMain {
val encryptedMessage: EncryptedPushMessage = mock()
val owner: LifecycleOwner = mock()
val lifecycle: Lifecycle = mock()
@@ -150,7 +153,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `subscribe calls native layer and notifies observers`() = runBlockingTest {
+ fun `subscribe calls native layer and notifies observers`() = runTestOnMain {
val connection: PushConnection = mock()
val subscription: AutoPushSubscription = mock()
var invoked = false
@@ -174,7 +177,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `subscribe invokes error callback`() = runBlockingTest {
+ fun `subscribe invokes error callback`() = runTestOnMain {
val connection: PushConnection = mock()
val subscription: AutoPushSubscription = mock()
var invoked = false
@@ -213,7 +216,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `unsubscribe calls native layer and notifies observers`() = runBlockingTest {
+ fun `unsubscribe calls native layer and notifies observers`() = runTestOnMain {
val connection: PushConnection = mock()
var invoked = false
var errorInvoked = false
@@ -266,7 +269,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `unsubscribe invokes error callback on native exception`() = runBlockingTest {
+ fun `unsubscribe invokes error callback on native exception`() = runTestOnMain {
val feature = AutoPushFeature(testContext, mock(), mock(), coroutineContext, connection)
var invoked = false
var errorInvoked = false
@@ -288,7 +291,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `getSubscription returns null when there is no subscription`() = runBlockingTest {
+ fun `getSubscription returns null when there is no subscription`() = runTestOnMain {
val feature = AutoPushFeature(testContext, mock(), mock(), coroutineContext, connection)
var invoked = false
@@ -305,7 +308,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `getSubscription invokes subscribe when there is a subscription`() = runBlockingTest {
+ fun `getSubscription invokes subscribe when there is a subscription`() = runTestOnMain {
val connection = TestPushConnection(true)
val feature = AutoPushFeature(testContext, mock(), mock(), coroutineContext, connection)
var invoked = false
@@ -321,7 +324,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `forceRegistrationRenewal deletes pref and calls service`() = runBlockingTest {
+ fun `forceRegistrationRenewal deletes pref and calls service`() = runTestOnMain {
val service: PushService = mock()
val feature = AutoPushFeature(testContext, service, mock(), coroutineContext, mock())
@@ -335,7 +338,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `verifyActiveSubscriptions notifies observers`() = runBlockingTest {
+ fun `verifyActiveSubscriptions notifies observers`() = runTestOnMain {
val connection: PushConnection = spy(TestPushConnection(true))
val owner: LifecycleOwner = mock()
val lifecycle: Lifecycle = mock()
@@ -365,7 +368,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `initialize executes verifyActiveSubscriptions after interval`() = runBlockingTest {
+ fun `initialize executes verifyActiveSubscriptions after interval`() = runTestOnMain {
val feature = spy(
AutoPushFeature(
context = testContext,
@@ -388,7 +391,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `initialize does not execute verifyActiveSubscription before interval`() = runBlockingTest {
+ fun `initialize does not execute verifyActiveSubscription before interval`() = runTestOnMain {
val feature = spy(
AutoPushFeature(
context = testContext,
@@ -413,7 +416,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `new FCM token executes verifyActiveSubscription`() = runBlockingTest {
+ fun `new FCM token executes verifyActiveSubscription`() = runTestOnMain {
val feature = spy(
AutoPushFeature(
context = testContext,
@@ -435,7 +438,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `verification doesn't happen until we've got the token`() = runBlockingTest {
+ fun `verification doesn't happen until we've got the token`() = runTestOnMain {
val feature = spy(
AutoPushFeature(
context = testContext,
@@ -452,7 +455,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `crash reporter is notified of errors`() = runBlockingTest {
+ fun `crash reporter is notified of errors`() = runTestOnMain {
val native: PushConnection = TestPushConnection(true)
val crashReporter: CrashReporting = mock()
val feature = AutoPushFeature(
@@ -470,7 +473,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `non-fatal errors are ignored`() = runBlockingTest {
+ fun `non-fatal errors are ignored`() = runTestOnMain {
val crashReporter: CrashReporting = mock()
val feature = AutoPushFeature(
context = testContext,
@@ -489,7 +492,7 @@ class AutoPushFeatureTest {
}
@Test
- fun `only fatal errors are reported`() = runBlockingTest {
+ fun `only fatal errors are reported`() = runTestOnMain {
val crashReporter: CrashReporting = mock()
val feature = AutoPushFeature(
context = testContext,
diff --git a/components/feature/push/src/test/java/mozilla/components/feature/push/RustPushConnectionTest.kt b/components/feature/push/src/test/java/mozilla/components/feature/push/RustPushConnectionTest.kt
index ca684173552..b349a7e4678 100644
--- a/components/feature/push/src/test/java/mozilla/components/feature/push/RustPushConnectionTest.kt
+++ b/components/feature/push/src/test/java/mozilla/components/feature/push/RustPushConnectionTest.kt
@@ -5,7 +5,8 @@
package mozilla.components.feature.push
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.appservices.push.DispatchInfo
import mozilla.appservices.push.KeyInfo
import mozilla.appservices.push.PushManager
@@ -30,48 +31,43 @@ import org.mockito.Mockito.anyString
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class RustPushConnectionTest {
@Ignore("Requires push-forUnitTests; seems unnecessary to introduce it for this one test.")
@Test
- fun `new token initializes API`() {
+ fun `new token initializes API`() = runTest {
val connection = createConnection()
assertNull(connection.api)
- runBlocking {
- connection.updateToken("token")
- }
+ connection.updateToken("token")
assertNotNull(connection.api)
}
@Test
- fun `new token calls update if API is already initialized`() {
+ fun `new token calls update if API is already initialized`() = runTest {
val connection = createConnection()
val api: PushManager = mock()
connection.api = api
- runBlocking {
- connection.updateToken("123")
- }
+ connection.updateToken("123")
verify(api, never()).subscribe(any(), any(), any())
verify(api).update(anyString())
}
@Test(expected = IllegalStateException::class)
- fun `subscribe throws if API is not initialized first`() {
+ fun `subscribe throws if API is not initialized first`() = runTest {
val connection = createConnection()
- runBlocking {
- connection.subscribe("123")
- }
+ connection.subscribe("123")
}
@Test
- fun `subscribe calls Rust API`() {
+ fun `subscribe calls Rust API`() = runTest {
val connection = createConnection()
val api: PushManager = mock()
val response = SubscriptionResponse(
@@ -89,64 +85,54 @@ class RustPushConnectionTest {
`when`(api.subscribe(anyString(), anyString(), nullable())).thenReturn(response)
- runBlocking {
- val sub = connection.subscribe("123")
+ val sub = connection.subscribe("123")
- assertEquals("123", sub.scope)
- assertEquals("auth", sub.authKey)
- assertEquals("p256dh", sub.publicKey)
- assertEquals("https://foo", sub.endpoint)
- }
+ assertEquals("123", sub.scope)
+ assertEquals("auth", sub.authKey)
+ assertEquals("p256dh", sub.publicKey)
+ assertEquals("https://foo", sub.endpoint)
verify(api).subscribe(anyString(), anyString(), nullable())
}
@Test(expected = IllegalStateException::class)
- fun `unsubscribe throws if API is not initialized first`() {
+ fun `unsubscribe throws if API is not initialized first`() = runTest {
val connection = createConnection()
- runBlocking {
- connection.unsubscribe("123")
- }
+ connection.unsubscribe("123")
}
@Test
- fun `unsubscribe calls Rust API`() {
+ fun `unsubscribe calls Rust API`() = runTest {
val connection = createConnection()
val api: PushManager = mock()
connection.api = api
- runBlocking {
- connection.unsubscribe("123")
- }
+ connection.unsubscribe("123")
verify(api).unsubscribe(anyString())
}
@Test(expected = IllegalStateException::class)
- fun `unsubscribeAll throws if API is not initialized first`() {
+ fun `unsubscribeAll throws if API is not initialized first`() = runTest {
val connection = createConnection()
- runBlocking {
- connection.unsubscribeAll()
- }
+ connection.unsubscribeAll()
}
@Test
- fun `unsubscribeAll calls Rust API`() {
+ fun `unsubscribeAll calls Rust API`() = runTest {
val connection = createConnection()
val api: PushManager = mock()
connection.api = api
- runBlocking {
- connection.unsubscribeAll()
- }
+ connection.unsubscribeAll()
verify(api).unsubscribeAll()
}
@Test
- fun `containsSubscription returns true if a subscription exists`() {
+ fun `containsSubscription returns true if a subscription exists`() = runTest {
val connection = createConnection()
val api: PushManager = mock()
connection.api = api
@@ -155,122 +141,99 @@ class RustPushConnectionTest {
.thenReturn(mock())
.thenReturn(null)
- runBlocking {
- assertTrue(connection.containsSubscription("validSubscription"))
-
- assertFalse(connection.containsSubscription("invalidSubscription"))
- }
+ assertTrue(connection.containsSubscription("validSubscription"))
+ assertFalse(connection.containsSubscription("invalidSubscription"))
}
@Test(expected = IllegalStateException::class)
- fun `verifyConnection throws if API is not initialized first`() {
+ fun `verifyConnection throws if API is not initialized first`() = runTest {
val connection = createConnection()
- runBlocking {
- connection.verifyConnection()
- }
+ connection.verifyConnection()
}
@Test
- fun `verifyConnection calls Rust API`() {
+ fun `verifyConnection calls Rust API`() = runTest {
val connection = createConnection()
val api: PushManager = mock()
connection.api = api
- runBlocking {
- connection.verifyConnection()
- }
+ connection.verifyConnection()
verify(api).verifyConnection()
}
@Test(expected = IllegalStateException::class)
- fun `decrypt throws if API is not initialized first`() {
+ fun `decrypt throws if API is not initialized first`() = runTest {
val connection = createConnection()
- runBlocking {
- connection.decryptMessage("123", "plain text")
- }
+ connection.decryptMessage("123", "plain text")
}
@Test
- fun `decrypt calls Rust API`() {
+ fun `decrypt calls Rust API`() = runTest {
val connection = createConnection()
val api: PushManager = mock()
val dispatchInfo: DispatchInfo = mock()
connection.api = api
- runBlocking {
- connection.decryptMessage("123", "body")
- }
+ connection.decryptMessage("123", "body")
verify(api, never()).decrypt(anyString(), anyString(), eq(""), eq(""), eq(""))
`when`(api.dispatchInfoForChid(anyString())).thenReturn(dispatchInfo)
`when`(dispatchInfo.scope).thenReturn("test")
- runBlocking {
- connection.decryptMessage("123", "body")
- }
+ connection.decryptMessage("123", "body")
verify(api).decrypt(anyString(), anyString(), eq(""), eq(""), eq(""))
- runBlocking {
- connection.decryptMessage("123", "body", "enc", "salt", "key")
- }
+ connection.decryptMessage("123", "body", "enc", "salt", "key")
verify(api).decrypt(anyString(), anyString(), eq("enc"), eq("salt"), eq("key"))
}
@Test
- fun `empty body decrypts nothing`() {
+ fun `empty body decrypts nothing`() = runTest {
val connection = createConnection()
val api: PushManager = mock()
val dispatchInfo: DispatchInfo = mock()
connection.api = api
- runBlocking {
- connection.decryptMessage("123", null)
- }
+ connection.decryptMessage("123", null)
verify(api, never()).decrypt(anyString(), anyString(), eq(""), eq(""), eq(""))
`when`(api.dispatchInfoForChid(anyString())).thenReturn(dispatchInfo)
`when`(dispatchInfo.scope).thenReturn("test")
- runBlocking {
- val (scope, message) = connection.decryptMessage("123", null)!!
- assertEquals("test", scope)
- assertNull(message)
- }
+ val (scope, message) = connection.decryptMessage("123", null)!!
+ assertEquals("test", scope)
+ assertNull(message)
verify(api, never()).decrypt(anyString(), nullable(), eq(""), eq(""), eq(""))
}
@Test(expected = IllegalStateException::class)
- fun `close throws if API is not initialized first`() {
+ fun `close throws if API is not initialized first`() = runTest {
val connection = createConnection()
- runBlocking {
- connection.close()
- }
+ connection.close()
}
@Test
- fun `close calls Rust API`() {
+ fun `close calls Rust API`() = runTest {
val connection = createConnection()
val api: PushManager = mock()
connection.api = api
- runBlocking {
- connection.close()
- }
+ connection.close()
verify(api).close()
}
@Test
- fun `initialized is true when api is not null`() {
+ fun `initialized is true when api is not null`() = runTest {
val connection = createConnection()
assertFalse(connection.isInitialized())
diff --git a/components/feature/push/src/test/java/mozilla/components/feature/push/ext/CoroutineScopeKtTest.kt b/components/feature/push/src/test/java/mozilla/components/feature/push/ext/CoroutineScopeKtTest.kt
index 7bb5dd3babf..9a4e560c180 100644
--- a/components/feature/push/src/test/java/mozilla/components/feature/push/ext/CoroutineScopeKtTest.kt
+++ b/components/feature/push/src/test/java/mozilla/components/feature/push/ext/CoroutineScopeKtTest.kt
@@ -8,7 +8,7 @@ package mozilla.components.feature.push.ext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.appservices.push.InternalException
import mozilla.appservices.push.PushException.AlreadyRegisteredException
import mozilla.appservices.push.PushException.CommunicationException
@@ -28,7 +28,7 @@ import org.junit.Test
class CoroutineScopeKtTest {
@Test(expected = InternalException::class)
- fun `launchAndTry throws on unrecoverable Rust exceptions`() = runBlockingTest {
+ fun `launchAndTry throws on unrecoverable Rust exceptions`() = runTest {
CoroutineScope(coroutineContext).launchAndTry(
errorBlock = { throw InternalException("unit test") },
block = { throw MissingRegistrationTokenException("") }
@@ -36,7 +36,7 @@ class CoroutineScopeKtTest {
}
@Test(expected = ArithmeticException::class)
- fun `launchAndTry throws original exception`() = runBlockingTest {
+ fun `launchAndTry throws original exception`() = runTest {
CoroutineScope(coroutineContext).launchAndTry(
errorBlock = { throw InternalException("unit test") },
block = { throw ArithmeticException() }
@@ -44,7 +44,7 @@ class CoroutineScopeKtTest {
}
@Test
- fun `launchAndTry should NOT throw on recoverable Rust exceptions`() = runBlockingTest {
+ fun `launchAndTry should NOT throw on recoverable Rust exceptions`() = runTest {
CoroutineScope(coroutineContext).launchAndTry(
{ throw CryptoException("should not fail test") },
{ assert(true) }
diff --git a/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt b/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt
index c115a3350dd..3857bc92d68 100644
--- a/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt
+++ b/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt
@@ -33,6 +33,7 @@ import mozilla.components.support.utils.toSafeIntent
/**
* Processor for intents which open Trusted Web Activities.
*/
+@Deprecated("TWAs are not supported. See https://github.com/mozilla-mobile/android-components/issues/12024")
class TrustedWebActivityIntentProcessor(
private val addNewTabUseCase: CustomTabsUseCases.AddCustomTabUseCase,
packageManager: PackageManager,
diff --git a/components/feature/pwa/src/main/res/values-ban/strings.xml b/components/feature/pwa/src/main/res/values-ban/strings.xml
new file mode 100644
index 00000000000..15441343cba
--- /dev/null
+++ b/components/feature/pwa/src/main/res/values-ban/strings.xml
@@ -0,0 +1,14 @@
+
+
+
+ Situs web
+
+
+ Kontrol situs layar penuh
+
+ Ketuk anggen nyalin URL ring aplikasi niki
+
+ Segerang
+
+ URL kasalin.
+
diff --git a/components/feature/pwa/src/main/res/values-skr/strings.xml b/components/feature/pwa/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..c22b7fc8f00
--- /dev/null
+++ b/components/feature/pwa/src/main/res/values-skr/strings.xml
@@ -0,0 +1,14 @@
+
+
+
+ ویب سائٹ
+
+
+ فل سکرین سائٹ کنٹرول
+
+ ایں ایپ کنوں یوآرایل دی نقل کرݨ کیتے انگل پھیرو
+
+ تازہ کرو
+
+ یوآرایل نقل تھی ڳیا۔
+
diff --git a/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ManifestStorageTest.kt b/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ManifestStorageTest.kt
index 7b4d36383ea..2bf5509d711 100644
--- a/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ManifestStorageTest.kt
+++ b/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/ManifestStorageTest.kt
@@ -6,7 +6,7 @@ package mozilla.components.feature.pwa
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.feature.pwa.db.ManifestDao
import mozilla.components.feature.pwa.db.ManifestEntity
@@ -50,14 +50,14 @@ class ManifestStorageTest {
)
@Test
- fun `load returns null if entry does not exist`() = runBlocking {
+ fun `load returns null if entry does not exist`() = runTest {
val storage = spy(ManifestStorage(testContext))
mockDatabase(storage)
assertNull(storage.loadManifest("https://example.com"))
}
@Test
- fun `load returns valid manifest`() = runBlocking {
+ fun `load returns valid manifest`() = runTest {
val storage = spy(ManifestStorage(testContext))
val dao = mockDatabase(storage)
@@ -69,7 +69,7 @@ class ManifestStorageTest {
}
@Test
- fun `save saves the manifest as JSON`() = runBlocking {
+ fun `save saves the manifest as JSON`() = runTest {
val storage = spy(ManifestStorage(testContext))
val dao = mockDatabase(storage)
@@ -79,7 +79,7 @@ class ManifestStorageTest {
}
@Test
- fun `update replaces the manifest as JSON`() = runBlocking {
+ fun `update replaces the manifest as JSON`() = runTest {
val storage = spy(ManifestStorage(testContext))
val dao = mockDatabase(storage)
val existing = ManifestEntity(firefoxManifest, currentTime = 0)
@@ -92,7 +92,7 @@ class ManifestStorageTest {
}
@Test
- fun `update does not replace non-existed manifest`() = runBlocking {
+ fun `update does not replace non-existed manifest`() = runTest {
val storage = spy(ManifestStorage(testContext))
val dao = mockDatabase(storage)
@@ -104,7 +104,7 @@ class ManifestStorageTest {
}
@Test
- fun `remove deletes saved manifests`() = runBlocking {
+ fun `remove deletes saved manifests`() = runTest {
val storage = spy(ManifestStorage(testContext))
val dao = mockDatabase(storage)
@@ -114,7 +114,7 @@ class ManifestStorageTest {
}
@Test
- fun `loading manifests by scope returns list of manifests`() = runBlocking {
+ fun `loading manifests by scope returns list of manifests`() = runTest {
val storage = spy(ManifestStorage(testContext))
val dao = mockDatabase(storage)
val manifest1 = WebAppManifest(name = "Mozilla1", startUrl = "https://mozilla.org", scope = "https://mozilla.org/pwa/1/")
@@ -131,7 +131,7 @@ class ManifestStorageTest {
}
@Test
- fun `loading manifests with share targets returns list of manifests`() = runBlocking {
+ fun `loading manifests with share targets returns list of manifests`() = runTest {
val storage = spy(ManifestStorage(testContext))
val dao = mockDatabase(storage)
val manifest1 = WebAppManifest(
@@ -158,7 +158,7 @@ class ManifestStorageTest {
}
@Test
- fun `updateManifestUsedAt updates usedAt to current timestamp`() = runBlocking {
+ fun `updateManifestUsedAt updates usedAt to current timestamp`() = runTest {
val storage = spy(ManifestStorage(testContext))
val dao = mockDatabase(storage)
val manifest = WebAppManifest(name = "Mozilla", startUrl = "https://mozilla.org")
@@ -178,7 +178,7 @@ class ManifestStorageTest {
}
@Test
- fun `has recent manifest returns false if no manifest is found`() = runBlocking {
+ fun `has recent manifest returns false if no manifest is found`() = runTest {
val storage = spy(ManifestStorage(testContext))
val dao = mockDatabase(storage)
val timeout = ManifestStorage.ACTIVE_THRESHOLD_MS
@@ -192,7 +192,7 @@ class ManifestStorageTest {
}
@Test
- fun `has recent manifest returns true if one or more manifests have been found`() = runBlocking {
+ fun `has recent manifest returns true if one or more manifests have been found`() = runTest {
val storage = spy(ManifestStorage(testContext))
val dao = mockDatabase(storage)
val timeout = ManifestStorage.ACTIVE_THRESHOLD_MS
@@ -211,7 +211,7 @@ class ManifestStorageTest {
}
@Test
- fun `recently used manifest count`() = runBlocking {
+ fun `recently used manifest count`() = runTest {
val testThreshold = 1000 * 60 * 24L
val storage = spy(ManifestStorage(testContext, activeThresholdMs = testThreshold))
val dao = mockDatabase(storage)
@@ -241,7 +241,7 @@ class ManifestStorageTest {
}
@Test
- fun `warmUpScopes populates cache of already installed web app scopes`() = runBlocking {
+ fun `warmUpScopes populates cache of already installed web app scopes`() = runTest {
val storage = spy(ManifestStorage(testContext))
val dao = mockDatabase(storage)
@@ -264,7 +264,7 @@ class ManifestStorageTest {
}
@Test
- fun `getInstalledScope returns cached scope for an url`() = runBlocking {
+ fun `getInstalledScope returns cached scope for an url`() = runTest {
val storage = spy(ManifestStorage(testContext))
val dao = mockDatabase(storage)
@@ -282,7 +282,7 @@ class ManifestStorageTest {
}
@Test
- fun `getStartUrlForInstalledScope returns cached start url for a currently installed scope`() = runBlocking {
+ fun `getStartUrlForInstalledScope returns cached start url for a currently installed scope`() = runTest {
val storage = spy(ManifestStorage(testContext))
val dao = mockDatabase(storage)
diff --git a/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppShortcutManagerTest.kt b/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppShortcutManagerTest.kt
index 8983fd1d4d5..c75ffa47502 100644
--- a/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppShortcutManagerTest.kt
+++ b/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppShortcutManagerTest.kt
@@ -14,7 +14,7 @@ import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.state.state.SecurityInfoState
import mozilla.components.browser.state.state.SessionState
@@ -78,7 +78,7 @@ class WebAppShortcutManagerTest {
fun teardown() = setSdkInt(0)
@Test
- fun `requestPinShortcut no-op if pinning unsupported`() = runBlockingTest {
+ fun `requestPinShortcut no-op if pinning unsupported`() = runTest {
val manifest = baseManifest.copy(
display = WebAppManifest.DisplayMode.STANDALONE,
icons = listOf(
@@ -103,7 +103,7 @@ class WebAppShortcutManagerTest {
}
@Test
- fun `requestPinShortcut won't make a PWA icon if the session is not installable`() = runBlockingTest {
+ fun `requestPinShortcut won't make a PWA icon if the session is not installable`() = runTest {
setSdkInt(Build.VERSION_CODES.O)
val manifest = baseManifest.copy(
display = WebAppManifest.DisplayMode.STANDALONE,
@@ -120,7 +120,7 @@ class WebAppShortcutManagerTest {
}
@Test
- fun `requestPinShortcut pins PWA shortcut`() = runBlockingTest {
+ fun `requestPinShortcut pins PWA shortcut`() = runTest {
setSdkInt(Build.VERSION_CODES.O)
val manifest = baseManifest.copy(
@@ -145,7 +145,7 @@ class WebAppShortcutManagerTest {
}
@Test
- fun `requestPinShortcut pins basic shortcut`() = runBlockingTest {
+ fun `requestPinShortcut pins basic shortcut`() = runTest {
setSdkInt(Build.VERSION_CODES.O)
val session = buildInstallableSession()
@@ -160,7 +160,7 @@ class WebAppShortcutManagerTest {
}
@Test
- fun `buildBasicShortcut uses manifest short name as label by default`() = runBlockingTest {
+ fun `buildBasicShortcut uses manifest short name as label by default`() = runTest {
setSdkInt(Build.VERSION_CODES.O)
val session = createTab("https://www.mozilla.org", title = "Internet for people, not profit — Mozilla").let {
@@ -181,7 +181,7 @@ class WebAppShortcutManagerTest {
}
@Test
- fun `buildBasicShortcut uses manifest name as label by default`() = runBlockingTest {
+ fun `buildBasicShortcut uses manifest name as label by default`() = runTest {
setSdkInt(Build.VERSION_CODES.O)
val session = createTab("https://www.mozilla.org", title = "Internet for people, not profit — Mozilla").let {
@@ -201,7 +201,7 @@ class WebAppShortcutManagerTest {
}
@Test
- fun `buildBasicShortcut uses session title as label if there is no manifest`() = runBlockingTest {
+ fun `buildBasicShortcut uses session title as label if there is no manifest`() = runTest {
setSdkInt(Build.VERSION_CODES.O)
val expectedTitle = "Internet for people, not profit — Mozilla"
@@ -214,7 +214,7 @@ class WebAppShortcutManagerTest {
}
@Test
- fun `buildBasicShortcut can create a shortcut with a custom name`() = runBlockingTest {
+ fun `buildBasicShortcut can create a shortcut with a custom name`() = runTest {
setSdkInt(Build.VERSION_CODES.O)
val title = "Internet for people, not profit — Mozilla"
@@ -228,7 +228,7 @@ class WebAppShortcutManagerTest {
}
@Test
- fun `updateShortcuts no-op`() = runBlockingTest {
+ fun `updateShortcuts no-op`() = runTest {
val manifests = listOf(baseManifest)
doReturn(null).`when`(manager).buildWebAppShortcut(context, manifests[0])
@@ -242,7 +242,7 @@ class WebAppShortcutManagerTest {
}
@Test
- fun `updateShortcuts updates list of existing shortcuts`() = runBlockingTest {
+ fun `updateShortcuts updates list of existing shortcuts`() = runTest {
setSdkInt(Build.VERSION_CODES.N_MR1)
val manifests = listOf(baseManifest)
val shortcutCompat: ShortcutInfoCompat = mock()
@@ -255,7 +255,7 @@ class WebAppShortcutManagerTest {
}
@Test
- fun `buildWebAppShortcut builds shortcut and saves manifest`() = runBlockingTest {
+ fun `buildWebAppShortcut builds shortcut and saves manifest`() = runTest {
doReturn(mock()).`when`(manager).buildIconFromManifest(baseManifest)
val shortcut = manager.buildWebAppShortcut(context, baseManifest)!!
@@ -271,7 +271,7 @@ class WebAppShortcutManagerTest {
}
@Test
- fun `buildWebAppShortcut builds shortcut with short name`() = runBlockingTest {
+ fun `buildWebAppShortcut builds shortcut with short name`() = runTest {
val manifest = WebAppManifest(name = "Demo Demo", shortName = "DD", startUrl = "https://example.com")
doReturn(mock()).`when`(manager).buildIconFromManifest(manifest)
@@ -303,7 +303,7 @@ class WebAppShortcutManagerTest {
}
@Test
- fun `checking unknown url returns uninstalled state`() = runBlockingTest {
+ fun `checking unknown url returns uninstalled state`() = runTest {
setSdkInt(Build.VERSION_CODES.N_MR1)
val url = "https://mozilla.org"
@@ -318,7 +318,7 @@ class WebAppShortcutManagerTest {
}
@Test
- fun `checking a known url returns installed state`() = runBlockingTest {
+ fun `checking a known url returns installed state`() = runTest {
setSdkInt(Build.VERSION_CODES.N_MR1)
val url = "https://mozilla.org/pwa/"
diff --git a/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppUseCasesTest.kt b/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppUseCasesTest.kt
index 1b979c78b51..b0ca535f958 100644
--- a/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppUseCasesTest.kt
+++ b/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/WebAppUseCasesTest.kt
@@ -6,7 +6,7 @@ package mozilla.components.feature.pwa
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.SecurityInfoState
import mozilla.components.browser.state.state.TabSessionState
@@ -109,7 +109,7 @@ class WebAppUseCasesTest {
}
@Test
- fun `getInstallState returns Installed if manifest exists`() = runBlockingTest {
+ fun `getInstallState returns Installed if manifest exists`() = runTest {
val httpClient: Client = mock()
val storage: ManifestStorage = mock()
val shortcutManager = WebAppShortcutManager(testContext, httpClient, storage)
diff --git a/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeatureTest.kt b/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeatureTest.kt
index 50ba0c0dab8..0624f872390 100644
--- a/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeatureTest.kt
+++ b/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/feature/ManifestUpdateFeatureTest.kt
@@ -5,12 +5,6 @@
package mozilla.components.feature.pwa.feature
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.runBlockingTest
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.createCustomTab
@@ -23,8 +17,10 @@ import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.never
@@ -33,10 +29,12 @@ import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class ManifestUpdateFeatureTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
private lateinit var shortcutManager: WebAppShortcutManager
private lateinit var storage: ManifestStorage
private lateinit var store: BrowserStore
- private lateinit var dispatcher: TestCoroutineDispatcher
private val sessionId = "external-app-session-id"
private val baseManifest = WebAppManifest(
@@ -50,9 +48,6 @@ class ManifestUpdateFeatureTest {
storage = mock()
shortcutManager = mock()
- dispatcher = TestCoroutineDispatcher()
- Dispatchers.setMain(dispatcher)
-
store = BrowserStore(
BrowserState(
customTabs = listOf(
@@ -62,15 +57,8 @@ class ManifestUpdateFeatureTest {
)
}
- @After
- fun tearDown() {
- dispatcher.cleanupTestCoroutines()
-
- Dispatchers.resetMain()
- }
-
@Test
- fun `start and stop handle null session`() = runBlockingTest {
+ fun `start and stop handle null session`() = runTestOnMain {
val feature = ManifestUpdateFeature(
testContext,
store,
@@ -83,7 +71,6 @@ class ManifestUpdateFeatureTest {
feature.start()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
feature.stop()
@@ -92,7 +79,7 @@ class ManifestUpdateFeatureTest {
}
@Test
- fun `Last usage is updated when feature is started`() {
+ fun `Last usage is updated when feature is started`() = runTestOnMain {
val feature = ManifestUpdateFeature(
testContext,
store,
@@ -114,13 +101,11 @@ class ManifestUpdateFeatureTest {
feature.updateUsageJob!!.joinBlocking()
- runBlocking {
- verify(storage).updateManifestUsedAt(baseManifest)
- }
+ verify(storage).updateManifestUsedAt(baseManifest)
}
@Test
- fun `updateStoredManifest is called when the manifest changes`() {
+ fun `updateStoredManifest is called when the manifest changes`() = runTestOnMain {
val feature = ManifestUpdateFeature(
testContext,
store,
@@ -150,16 +135,13 @@ class ManifestUpdateFeatureTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
feature.updateJob!!.joinBlocking()
- runBlocking {
- verify(storage).updateManifest(newManifest)
- }
+ verify(storage).updateManifest(newManifest)
}
@Test
- fun `updateStoredManifest is not called when the manifest is the same`() {
+ fun `updateStoredManifest is not called when the manifest is the same`() = runTestOnMain {
val feature = ManifestUpdateFeature(
testContext,
store,
@@ -179,16 +161,13 @@ class ManifestUpdateFeatureTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
feature.updateJob?.joinBlocking()
- runBlocking {
- verify(storage, never()).updateManifest(any())
- }
+ verify(storage, never()).updateManifest(any())
}
@Test
- fun `updateStoredManifest is not called when the manifest is removed`() {
+ fun `updateStoredManifest is not called when the manifest is removed`() = runTestOnMain {
val feature = ManifestUpdateFeature(
testContext,
store,
@@ -215,16 +194,13 @@ class ManifestUpdateFeatureTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
feature.updateJob?.joinBlocking()
- runBlocking {
- verify(storage, never()).updateManifest(any())
- }
+ verify(storage, never()).updateManifest(any())
}
@Test
- fun `updateStoredManifest is not called when the manifest has a different start URL`() {
+ fun `updateStoredManifest is not called when the manifest has a different start URL`() = runTestOnMain {
val feature = ManifestUpdateFeature(
testContext,
store,
@@ -252,16 +228,13 @@ class ManifestUpdateFeatureTest {
)
).joinBlocking()
- dispatcher.advanceUntilIdle()
feature.updateJob?.joinBlocking()
- runBlocking {
- verify(storage, never()).updateManifest(any())
- }
+ verify(storage, never()).updateManifest(any())
}
@Test
- fun `updateStoredManifest updates storage and shortcut`() = runBlockingTest {
+ fun `updateStoredManifest updates storage and shortcut`() = runTestOnMain {
val feature = ManifestUpdateFeature(testContext, store, shortcutManager, storage, sessionId, baseManifest)
val manifest = baseManifest.copy(shortName = "Moz")
@@ -272,7 +245,7 @@ class ManifestUpdateFeatureTest {
}
@Test
- fun `start updates last web app usage`() = runBlockingTest {
+ fun `start updates last web app usage`() = runTestOnMain {
val feature = ManifestUpdateFeature(testContext, store, shortcutManager, storage, sessionId, baseManifest)
feature.start()
diff --git a/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessorTest.kt b/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessorTest.kt
index fcb0ced8526..24d763c034a 100644
--- a/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessorTest.kt
+++ b/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessorTest.kt
@@ -23,12 +23,15 @@ import mozilla.components.support.test.mock
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
@ExperimentalCoroutinesApi
+@Suppress("DEPRECATION")
+@Ignore("TrustedWebActivityIntentProcessorTest] is deprecated. See https://github.com/mozilla-mobile/android-components/issues/12024")
class TrustedWebActivityIntentProcessorTest {
private lateinit var store: BrowserStore
diff --git a/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt b/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt
index e81466b4b88..958d048e94a 100644
--- a/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt
+++ b/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt
@@ -9,7 +9,7 @@ import android.content.Intent.ACTION_VIEW
import androidx.core.net.toUri
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.state.CustomTabConfig
import mozilla.components.browser.state.state.ExternalAppType
import mozilla.components.browser.state.state.SessionState
@@ -50,7 +50,7 @@ class WebAppIntentProcessorTest {
}
@Test
- fun `process returns false if no manifest is in storage`() = runBlockingTest {
+ fun `process returns false if no manifest is in storage`() = runTest {
val storage: ManifestStorage = mock()
val processor = WebAppIntentProcessor(mock(), mock(), mock(), storage)
@@ -60,7 +60,7 @@ class WebAppIntentProcessorTest {
}
@Test
- fun `process adds session ID and manifest to intent`() = runBlockingTest {
+ fun `process adds session ID and manifest to intent`() = runTest {
val store = BrowserStore()
val storage: ManifestStorage = mock()
@@ -97,7 +97,7 @@ class WebAppIntentProcessorTest {
}
@Test
- fun `process adds custom tab config`() = runBlockingTest {
+ fun `process adds custom tab config`() = runTest {
val intent = Intent(ACTION_VIEW_PWA, "https://mozilla.com".toUri())
val storage: ManifestStorage = mock()
@@ -129,7 +129,7 @@ class WebAppIntentProcessorTest {
}
@Test
- fun `url override is applied to session if present`() = runBlockingTest {
+ fun `url override is applied to session if present`() = runTest {
val store = BrowserStore()
val storage: ManifestStorage = mock()
diff --git a/components/feature/qr/src/main/res/values-skr/strings.xml b/components/feature/qr/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..e44642f62cd
--- /dev/null
+++ b/components/feature/qr/src/main/res/values-skr/strings.xml
@@ -0,0 +1,10 @@
+
+
+
+
+ QR سکینر
+
+
+ ڈیوائس تے کوئی کیمرہ دستیاب کائنی
+
+
diff --git a/components/feature/qr/src/main/res/values-ug/strings.xml b/components/feature/qr/src/main/res/values-ug/strings.xml
new file mode 100644
index 00000000000..67ff29d85d9
--- /dev/null
+++ b/components/feature/qr/src/main/res/values-ug/strings.xml
@@ -0,0 +1,10 @@
+
+
+
+
+ QR سايىلىغۇچ
+
+
+ كامېرا يوق ئىكەن
+
+
diff --git a/components/feature/readerview/src/main/res/values-skr/strings.xml b/components/feature/readerview/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..34b1b31c0dd
--- /dev/null
+++ b/components/feature/readerview/src/main/res/values-skr/strings.xml
@@ -0,0 +1,29 @@
+
+
+
+ سینس سیرف
+
+ سانس سیرف فونٹ
+
+ سیرف
+
+ سیرف فونٹ
+
+
+ فونٹ سائز گھٹاؤ
+
+
+ فونٹ سائز ودھاؤ
+
+ شوخ
+
+ شوخ رنگ سکیم
+
+ سیپیا
+
+ سیپیا رنگ سکیم
+
+ پھکا
+
+ پھکا رنگ سکیم
+
diff --git a/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/ReaderViewFeatureTest.kt b/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/ReaderViewFeatureTest.kt
index 723de6837d2..82a58c32d29 100644
--- a/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/ReaderViewFeatureTest.kt
+++ b/components/feature/readerview/src/test/java/mozilla/components/feature/readerview/ReaderViewFeatureTest.kt
@@ -60,7 +60,6 @@ class ReaderViewFeatureTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val testDispatcher = coroutinesTestRule.testDispatcher
@Before
fun setup() {
@@ -194,7 +193,6 @@ class ReaderViewFeatureTest {
store.dispatch(ReaderAction.UpdateReaderableCheckRequiredAction(tab.id, true)).joinBlocking()
- testDispatcher.advanceUntilIdle()
val tabCaptor = argumentCaptor()
verify(readerViewFeature).checkReaderState(tabCaptor.capture())
assertEquals(tab.id, tabCaptor.value.id)
@@ -209,7 +207,6 @@ class ReaderViewFeatureTest {
readerViewFeature.start()
store.dispatch(ReaderAction.UpdateReaderConnectRequiredAction(tab.id, true)).joinBlocking()
- testDispatcher.advanceUntilIdle()
val tabCaptor = argumentCaptor()
verify(readerViewFeature).connectReaderViewContentScript(tabCaptor.capture())
assertEquals(tab.id, tabCaptor.value.id)
@@ -232,27 +229,22 @@ class ReaderViewFeatureTest {
store.dispatch(TabListAction.SelectTabAction(tab.id)).joinBlocking()
store.dispatch(ReaderAction.UpdateReaderableAction(tab.id, true)).joinBlocking()
- testDispatcher.advanceUntilIdle()
assertEquals(1, readerViewStatusChanges.size)
assertEquals(Pair(true, false), readerViewStatusChanges[0])
store.dispatch(ReaderAction.UpdateReaderActiveAction(tab.id, true)).joinBlocking()
- testDispatcher.advanceUntilIdle()
assertEquals(2, readerViewStatusChanges.size)
assertEquals(Pair(true, true), readerViewStatusChanges[1])
store.dispatch(ReaderAction.UpdateReaderableAction(tab.id, true)).joinBlocking()
- testDispatcher.advanceUntilIdle()
// No change -> No notification should have been sent
assertEquals(2, readerViewStatusChanges.size)
store.dispatch(ReaderAction.UpdateReaderActiveAction(tab.id, false)).joinBlocking()
- testDispatcher.advanceUntilIdle()
assertEquals(3, readerViewStatusChanges.size)
assertEquals(Pair(true, false), readerViewStatusChanges[2])
store.dispatch(ReaderAction.UpdateReaderableAction(tab.id, false)).joinBlocking()
- testDispatcher.advanceUntilIdle()
assertEquals(4, readerViewStatusChanges.size)
assertEquals(Pair(false, false), readerViewStatusChanges[3])
}
@@ -317,7 +309,6 @@ class ReaderViewFeatureTest {
store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession)).joinBlocking()
store.dispatch(TabListAction.SelectTabAction(tab.id)).joinBlocking()
store.dispatch(ContentAction.UpdateBackNavigationStateAction(tab.id, true)).joinBlocking()
- testDispatcher.advanceUntilIdle()
readerViewFeature.hideReaderView()
verify(engineSession).goBack(false)
@@ -496,7 +487,6 @@ class ReaderViewFeatureTest {
val message = argumentCaptor()
readerViewFeature.start()
store.dispatch(ReaderAction.UpdateReaderConnectRequiredAction(tab.id, true)).joinBlocking()
- testDispatcher.advanceUntilIdle()
verify(controller).registerContentMessageHandler(
eq(engineSession), messageHandler.capture(), eq(READER_VIEW_ACTIVE_CONTENT_PORT)
)
@@ -551,7 +541,6 @@ class ReaderViewFeatureTest {
readerViewFeature.start()
store.dispatch(ReaderAction.UpdateReaderConnectRequiredAction(tab.id, true)).joinBlocking()
- testDispatcher.advanceUntilIdle()
verify(controller).registerContentMessageHandler(
eq(engineSession), messageHandler.capture(), eq(READER_VIEW_ACTIVE_CONTENT_PORT)
)
diff --git a/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedMiddlewareTest.kt b/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedMiddlewareTest.kt
index fa3c04a15ff..3c14781e21f 100644
--- a/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedMiddlewareTest.kt
+++ b/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedMiddlewareTest.kt
@@ -5,11 +5,8 @@
package mozilla.components.feature.recentlyclosed
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.action.UndoAction
@@ -25,10 +22,12 @@ import mozilla.components.support.test.eq
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import mozilla.components.support.test.whenever
-import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.times
@@ -41,13 +40,10 @@ class RecentlyClosedMiddlewareTest {
lateinit var store: BrowserStore
lateinit var engine: Engine
- private val dispatcher = TestCoroutineDispatcher()
- private val scope = CoroutineScope(dispatcher)
-
- @After
- fun tearDown() {
- dispatcher.cleanupTestCoroutines()
- }
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
@Before
fun setup() {
@@ -67,7 +63,7 @@ class RecentlyClosedMiddlewareTest {
)
@Test
- fun `closed tab storage stores the provided tab on add tab action`() = runBlocking {
+ fun `closed tab storage stores the provided tab on add tab action`() = runTestOnMain {
val storage = mockStorage()
val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
@@ -77,7 +73,7 @@ class RecentlyClosedMiddlewareTest {
)
store.dispatch(RecentlyClosedAction.AddClosedTabsAction(listOf(closedTab))).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(storage).addTabsToCollectionWithMax(
@@ -86,7 +82,7 @@ class RecentlyClosedMiddlewareTest {
}
@Test
- fun `closed tab storage adds normal tabs removed with TabListAction`() = runBlocking {
+ fun `closed tab storage adds normal tabs removed with TabListAction`() = runTestOnMain {
val storage = mockStorage()
val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
@@ -103,7 +99,7 @@ class RecentlyClosedMiddlewareTest {
store.dispatch(TabListAction.RemoveTabsAction(listOf("1234", "5678"))).joinBlocking()
store.dispatch(UndoAction.ClearRecoverableTabs(store.state.undoHistory.tag)).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
val closedTabCaptor = argumentCaptor>()
@@ -127,7 +123,7 @@ class RecentlyClosedMiddlewareTest {
}
@Test
- fun `closed tab storage adds a normal tab removed with TabListAction`() = runBlocking {
+ fun `closed tab storage adds a normal tab removed with TabListAction`() = runTestOnMain {
val storage = mockStorage()
val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
@@ -143,7 +139,7 @@ class RecentlyClosedMiddlewareTest {
store.dispatch(TabListAction.RemoveTabAction("1234")).joinBlocking()
store.dispatch(UndoAction.ClearRecoverableTabs(store.state.undoHistory.tag)).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
val closedTabCaptor = argumentCaptor>()
@@ -161,7 +157,7 @@ class RecentlyClosedMiddlewareTest {
}
@Test
- fun `closed tab storage does not add a private tab removed with TabListAction`() = runBlocking {
+ fun `closed tab storage does not add a private tab removed with TabListAction`() = runTestOnMain {
val storage = mockStorage()
val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
@@ -175,7 +171,7 @@ class RecentlyClosedMiddlewareTest {
)
store.dispatch(TabListAction.RemoveTabAction("1234")).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(storage).getTabs()
@@ -183,7 +179,7 @@ class RecentlyClosedMiddlewareTest {
}
@Test
- fun `closed tab storage adds all normals tab removed with TabListAction RemoveAllNormalTabsAction`() = runBlocking {
+ fun `closed tab storage adds all normals tab removed with TabListAction RemoveAllNormalTabsAction`() = runTestOnMain {
val storage = mockStorage()
val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
@@ -200,7 +196,7 @@ class RecentlyClosedMiddlewareTest {
store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking()
store.dispatch(UndoAction.ClearRecoverableTabs(store.state.undoHistory.tag)).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
val closedTabCaptor = argumentCaptor>()
@@ -218,7 +214,7 @@ class RecentlyClosedMiddlewareTest {
}
@Test
- fun `closed tab storage adds all normal tabs and no private tabs removed with TabListAction RemoveAllTabsAction`() = runBlocking {
+ fun `closed tab storage adds all normal tabs and no private tabs removed with TabListAction RemoveAllTabsAction`() = runTestOnMain {
val storage = mockStorage()
val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
@@ -235,7 +231,7 @@ class RecentlyClosedMiddlewareTest {
store.dispatch(TabListAction.RemoveAllTabsAction()).joinBlocking()
store.dispatch(UndoAction.ClearRecoverableTabs(store.state.undoHistory.tag)).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
val closedTabCaptor = argumentCaptor>()
@@ -253,7 +249,7 @@ class RecentlyClosedMiddlewareTest {
}
@Test
- fun `closed tabs storage adds tabs closed one after the other without clear actions in between`() = runBlocking {
+ fun `closed tabs storage adds tabs closed one after the other without clear actions in between`() = runTestOnMain {
val storage = mockStorage()
val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
@@ -278,7 +274,7 @@ class RecentlyClosedMiddlewareTest {
assertEquals(1, store.state.tabs.size)
assertEquals("tab4", store.state.selectedTabId)
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
val closedTabCaptor = argumentCaptor>()
@@ -311,7 +307,7 @@ class RecentlyClosedMiddlewareTest {
}
@Test
- fun `fetch the tabs from the recently closed storage and load into browser state on initialize tab state action`() = runBlocking {
+ fun `fetch the tabs from the recently closed storage and load into browser state on initialize tab state action`() = runTestOnMain {
val storage = mockStorage(tabs = listOf(closedTab.state))
val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
@@ -321,7 +317,7 @@ class RecentlyClosedMiddlewareTest {
store.waitUntilIdle()
// Now wait for Middleware to process Init action and store to process action from middleware
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(storage).getTabs()
@@ -329,7 +325,7 @@ class RecentlyClosedMiddlewareTest {
}
@Test
- fun `recently closed storage removes the provided tab on remove tab action`() = runBlocking {
+ fun `recently closed storage removes the provided tab on remove tab action`() = runTestOnMain {
val storage = mockStorage()
val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
@@ -343,16 +339,16 @@ class RecentlyClosedMiddlewareTest {
)
store.dispatch(RecentlyClosedAction.RemoveClosedTabAction(closedTab.state)).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(storage).removeTab(closedTab.state)
}
@Test
- fun `recently closed storage removes all tabs on remove all tabs action`() = runBlocking {
+ fun `recently closed storage removes all tabs on remove all tabs action`() = runTestOnMain {
val storage = mockStorage()
val middleware = RecentlyClosedMiddleware(lazy { storage }, 5, scope)
val store = BrowserStore(
@@ -365,10 +361,10 @@ class RecentlyClosedMiddlewareTest {
)
store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
verify(storage).removeAllTabs()
}
diff --git a/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabDaoTest.kt b/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabDaoTest.kt
index 418c3371a8d..05bcc323382 100644
--- a/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabDaoTest.kt
+++ b/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabDaoTest.kt
@@ -8,21 +8,27 @@ import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.runBlocking
import mozilla.components.feature.recentlyclosed.db.RecentlyClosedTabDao
import mozilla.components.feature.recentlyclosed.db.RecentlyClosedTabEntity
import mozilla.components.feature.recentlyclosed.db.RecentlyClosedTabsDatabase
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.UUID
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class RecentlyClosedTabDaoTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
private val context: Context
get() = ApplicationProvider.getApplicationContext()
@@ -31,13 +37,15 @@ class RecentlyClosedTabDaoTest {
@Before
fun setUp() {
- database =
- Room.inMemoryDatabaseBuilder(context, RecentlyClosedTabsDatabase::class.java).build()
+ database = Room
+ .inMemoryDatabaseBuilder(context, RecentlyClosedTabsDatabase::class.java)
+ .allowMainThreadQueries()
+ .build()
tabDao = database.recentlyClosedTabDao()
}
@Test
- fun testAddingTabs() = runBlocking(Dispatchers.IO) {
+ fun testAddingTabs() = runTestOnMain {
val tab1 = RecentlyClosedTabEntity(
title = "RecentlyClosedTab One",
url = "https://www.mozilla.org",
@@ -65,7 +73,7 @@ class RecentlyClosedTabDaoTest {
}
@Test
- fun testRemovingTab() = runBlocking(Dispatchers.IO) {
+ fun testRemovingTab() = runTestOnMain {
val tab1 = RecentlyClosedTabEntity(
title = "RecentlyClosedTab One",
url = "https://www.mozilla.org",
@@ -94,7 +102,7 @@ class RecentlyClosedTabDaoTest {
}
@Test
- fun testRemovingAllTabs() = runBlocking(Dispatchers.IO) {
+ fun testRemovingAllTabs() = runTestOnMain {
RecentlyClosedTabEntity(
title = "RecentlyClosedTab One",
url = "https://www.mozilla.org",
diff --git a/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorageTest.kt b/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorageTest.kt
index ba8a38b3eab..260f15a0132 100644
--- a/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorageTest.kt
+++ b/components/feature/recentlyclosed/src/test/java/mozilla/components/feature/recentlyclosed/RecentlyClosedTabsStorageTest.kt
@@ -6,9 +6,8 @@ package mozilla.components.feature.recentlyclosed
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.runBlocking
import mozilla.components.browser.state.state.recover.RecoverableTab
import mozilla.components.browser.state.state.recover.TabState
import mozilla.components.concept.base.crash.CrashReporting
@@ -18,19 +17,24 @@ import mozilla.components.feature.recentlyclosed.db.RecentlyClosedTabsDatabase
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.fail
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify
import java.io.IOException
-import java.lang.Exception
-import java.lang.IllegalStateException
+@ExperimentalCoroutinesApi // for runTestOnMain
@RunWith(AndroidJUnit4::class)
class RecentlyClosedTabsStorageTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
private lateinit var storage: RecentlyClosedTabsStorage
private lateinit var engineStateStorage: TestEngineSessionStateStorage
private lateinit var database: RecentlyClosedTabsDatabase
@@ -68,6 +72,7 @@ class RecentlyClosedTabsStorageTest {
crashReporting = mock()
database = Room
.inMemoryDatabaseBuilder(testContext, RecentlyClosedTabsDatabase::class.java)
+ .allowMainThreadQueries()
.build()
engineStateStorage = TestEngineSessionStateStorage()
@@ -87,7 +92,7 @@ class RecentlyClosedTabsStorageTest {
}
@Test
- fun testAddingTabsWithMax() {
+ fun testAddingTabsWithMax() = runTestOnMain {
// Test tab
val t1 = System.currentTimeMillis()
val closedTab = RecoverableTab(
@@ -112,10 +117,8 @@ class RecentlyClosedTabsStorageTest {
)
)
- val tabs = runBlocking(Dispatchers.IO) {
- storage.addTabsToCollectionWithMax(listOf(closedTab, secondClosedTab), 1)
- storage.getTabs().first()
- }
+ storage.addTabsToCollectionWithMax(listOf(closedTab, secondClosedTab), 1)
+ val tabs = storage.getTabs().first()
assertEquals(1, engineStateStorage.data.size)
assertEquals(engineState2, engineStateStorage.data["second-tab"])
@@ -137,10 +140,8 @@ class RecentlyClosedTabsStorageTest {
)
)
- val newTabs = runBlocking(Dispatchers.IO) {
- storage.addTabsToCollectionWithMax(listOf(thirdClosedTab), 1)
- storage.getTabs().first()
- }
+ storage.addTabsToCollectionWithMax(listOf(thirdClosedTab), 1)
+ val newTabs = storage.getTabs().first()
assertEquals(1, engineStateStorage.data.size)
assertEquals(engineState3, engineStateStorage.data["third-tab"])
@@ -152,7 +153,7 @@ class RecentlyClosedTabsStorageTest {
}
@Test
- fun testAllowAddingSameTabTwice() {
+ fun testAllowAddingSameTabTwice() = runTestOnMain {
// Test tab
val engineState: EngineSessionState = mock()
val closedTab = RecoverableTab(
@@ -166,11 +167,9 @@ class RecentlyClosedTabsStorageTest {
)
val updatedTab = closedTab.copy(state = closedTab.state.copy(title = "updated"))
- val tabs = runBlocking(Dispatchers.IO) {
- storage.addTabsToCollectionWithMax(listOf(closedTab), 2)
- storage.addTabsToCollectionWithMax(listOf(updatedTab), 2)
- storage.getTabs().first()
- }
+ storage.addTabsToCollectionWithMax(listOf(closedTab), 2)
+ storage.addTabsToCollectionWithMax(listOf(updatedTab), 2)
+ val tabs = storage.getTabs().first()
assertEquals(1, engineStateStorage.data.size)
assertEquals(engineState, engineStateStorage.data["first-tab"])
@@ -182,7 +181,7 @@ class RecentlyClosedTabsStorageTest {
}
@Test
- fun testRemovingAllTabs() {
+ fun testRemovingAllTabs() = runTestOnMain {
// Test tab
val t1 = System.currentTimeMillis()
val closedTab = RecoverableTab(
@@ -206,10 +205,8 @@ class RecentlyClosedTabsStorageTest {
)
)
- val tabs = runBlocking(Dispatchers.IO) {
- storage.addTabsToCollectionWithMax(listOf(closedTab, secondClosedTab), 2)
- storage.getTabs().first()
- }
+ storage.addTabsToCollectionWithMax(listOf(closedTab, secondClosedTab), 2)
+ val tabs = storage.getTabs().first()
assertEquals(2, engineStateStorage.data.size)
assertEquals(2, tabs.size)
@@ -220,17 +217,15 @@ class RecentlyClosedTabsStorageTest {
assertEquals(secondClosedTab.state.title, tabs[1].title)
assertEquals(secondClosedTab.state.lastAccess, tabs[1].lastAccess)
- val newTabs = runBlocking(Dispatchers.IO) {
- storage.removeAllTabs()
- storage.getTabs().first()
- }
+ storage.removeAllTabs()
+ val newTabs = storage.getTabs().first()
assertEquals(0, engineStateStorage.data.size)
assertEquals(0, newTabs.size)
}
@Test
- fun testRemovingOneTab() {
+ fun testRemovingOneTab() = runTestOnMain {
// Test tab
val engineState1: EngineSessionState = mock()
val t1 = System.currentTimeMillis()
@@ -256,11 +251,9 @@ class RecentlyClosedTabsStorageTest {
)
)
- val tabs = runBlocking(Dispatchers.IO) {
- storage.addTabState(closedTab)
- storage.addTabState(secondClosedTab)
- storage.getTabs().first()
- }
+ storage.addTabState(closedTab)
+ storage.addTabState(secondClosedTab)
+ val tabs = storage.getTabs().first()
assertEquals(2, engineStateStorage.data.size)
assertEquals(2, tabs.size)
@@ -271,10 +264,8 @@ class RecentlyClosedTabsStorageTest {
assertEquals(secondClosedTab.state.title, tabs[1].title)
assertEquals(secondClosedTab.state.lastAccess, tabs[1].lastAccess)
- val newTabs = runBlocking(Dispatchers.IO) {
- storage.removeTab(tabs[0])
- storage.getTabs().first()
- }
+ storage.removeTab(tabs[0])
+ val newTabs = storage.getTabs().first()
assertEquals(1, engineStateStorage.data.size)
assertEquals(engineState2, engineStateStorage.data["second-tab"])
@@ -285,7 +276,7 @@ class RecentlyClosedTabsStorageTest {
}
@Test
- fun testAddingTabWithEngineStateStorageFailure() {
+ fun testAddingTabWithEngineStateStorageFailure() = runTestOnMain {
// 'fail' in tab's id will cause test engine session storage to fail on writing engineSessionState.
val closedTab = RecoverableTab(
engineSessionState = mock(),
@@ -297,10 +288,8 @@ class RecentlyClosedTabsStorageTest {
)
)
- val tabs = runBlocking(Dispatchers.IO) {
- storage.addTabState(closedTab)
- storage.getTabs().first()
- }
+ storage.addTabState(closedTab)
+ val tabs = storage.getTabs().first()
// if it's empty, we know state write failed
assertEquals(0, engineStateStorage.data.size)
// but the tab was still written into the database.
@@ -313,7 +302,7 @@ class RecentlyClosedTabsStorageTest {
}
@Test
- fun testStorageFailuresAreCaught() {
+ fun testStorageFailuresAreCaught() = runTestOnMain {
val engineState: EngineSessionState = mock()
val closedTab = RecoverableTab(
engineSessionState = engineState,
@@ -324,13 +313,11 @@ class RecentlyClosedTabsStorageTest {
lastAccess = System.currentTimeMillis()
)
)
- runBlocking(Dispatchers.IO) {
- try {
- storage.addTabsToCollectionWithMax(listOf(closedTab), 2)
- verify(crashReporting).submitCaughtException(any())
- } catch (e: Exception) {
- fail("Thrown exception was not caught")
- }
+ try {
+ storage.addTabsToCollectionWithMax(listOf(closedTab), 2)
+ verify(crashReporting).submitCaughtException(any())
+ } catch (e: Exception) {
+ fail("Thrown exception was not caught")
}
}
}
diff --git a/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt b/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt
index 5fdf16c7663..0a3c656b5d4 100644
--- a/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt
+++ b/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt
@@ -180,6 +180,8 @@ class SearchUseCases(
SearchEngine.Type.CUSTOM -> store.dispatch(
SearchAction.UpdateCustomSearchEngineAction(searchEngine)
)
+
+ SearchEngine.Type.APPLICATION -> { /* Do nothing */ }
}
}
}
@@ -209,6 +211,8 @@ class SearchUseCases(
SearchEngine.Type.CUSTOM -> store.dispatch(
SearchAction.RemoveCustomSearchEngineAction(searchEngine.id)
)
+
+ SearchEngine.Type.APPLICATION -> { /* Do nothing */ }
}
}
}
diff --git a/components/feature/search/src/main/java/mozilla/components/feature/search/ext/SearchEngine.kt b/components/feature/search/src/main/java/mozilla/components/feature/search/ext/SearchEngine.kt
index ba0fe85445f..d960aaa41f6 100644
--- a/components/feature/search/src/main/java/mozilla/components/feature/search/ext/SearchEngine.kt
+++ b/components/feature/search/src/main/java/mozilla/components/feature/search/ext/SearchEngine.kt
@@ -41,6 +41,26 @@ fun createSearchEngine(
)
}
+/**
+ * Creates an application [SearchEngine].
+ */
+fun createApplicationSearchEngine(
+ id: String? = null,
+ name: String,
+ url: String,
+ icon: Bitmap,
+ suggestUrl: String? = null,
+): SearchEngine {
+ return SearchEngine(
+ id = id ?: UUID.randomUUID().toString(),
+ name = name,
+ icon = icon,
+ type = SearchEngine.Type.APPLICATION,
+ resultUrls = listOf(url),
+ suggestUrl = suggestUrl,
+ )
+}
+
/**
* Whether this [SearchEngine] has a [SearchEngine.suggestUrl] set and can provide search
* suggestions.
diff --git a/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/SearchMiddleware.kt b/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/SearchMiddleware.kt
index b101e912ef1..4b3f627df58 100644
--- a/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/SearchMiddleware.kt
+++ b/components/feature/search/src/main/java/mozilla/components/feature/search/middleware/SearchMiddleware.kt
@@ -28,10 +28,15 @@ import kotlin.coroutines.CoroutineContext
/**
* [Middleware] implementation for loading and saving [SearchEngine]s whenever the state changes.
*
- * @param context The application context.
* @param additionalBundledSearchEngineIds List of (bundled) search engine IDs that will be loaded
* in addition to the search engines for the user's region and made available through
* [SearchState.additionalSearchEngines] and [SearchState.additionalSearchEngines].
+ * @param migration Interface for a class that can provide data from a legacy system to be imported into the
+ * storage used by the middleware.
+ * @param customStorage A storage for custom search engines of the user.
+ * @param bundleStorage A storage for loading bundled search engines.
+ * @param metadataStorage A storage for saving additional metadata related to search.
+ * @param ioDispatcher The coroutine dispatcher to be used when loading.
*/
@Suppress("LongParameterList")
class SearchMiddleware(
@@ -41,7 +46,7 @@ class SearchMiddleware(
private val customStorage: CustomStorage = CustomSearchEngineStorage(context),
private val bundleStorage: BundleStorage = BundledSearchEnginesStorage(context),
private val metadataStorage: MetadataStorage = SearchMetadataStorage(context),
- private val ioDispatcher: CoroutineContext = Dispatchers.IO
+ private val ioDispatcher: CoroutineContext = Dispatchers.IO,
) : Middleware {
private val logger = Logger("SearchMiddleware")
private val scope = CoroutineScope(ioDispatcher)
diff --git a/components/feature/search/src/test/java/mozilla/components/feature/search/SearchUseCasesTest.kt b/components/feature/search/src/test/java/mozilla/components/feature/search/SearchUseCasesTest.kt
index 5331c3b5a39..71700700568 100644
--- a/components/feature/search/src/test/java/mozilla/components/feature/search/SearchUseCasesTest.kt
+++ b/components/feature/search/src/test/java/mozilla/components/feature/search/SearchUseCasesTest.kt
@@ -206,6 +206,9 @@ class SearchUseCasesTest {
SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM)
),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION)
+ ),
additionalSearchEngines = listOf(
SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL)
),
@@ -268,6 +271,9 @@ class SearchUseCasesTest {
SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM)
),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION)
+ ),
additionalSearchEngines = listOf(
SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL)
),
@@ -287,7 +293,7 @@ class SearchUseCasesTest {
val useCases = SearchUseCases(store, mock(), mock())
- assertEquals(6, store.state.search.searchEngines.size)
+ assertEquals(7, store.state.search.searchEngines.size)
assertEquals(3, store.state.search.availableSearchEngines.size)
useCases.addSearchEngine.invoke(
@@ -296,7 +302,7 @@ class SearchUseCasesTest {
store.waitUntilIdle()
- assertEquals(7, store.state.search.searchEngines.size)
+ assertEquals(8, store.state.search.searchEngines.size)
assertEquals(2, store.state.search.availableSearchEngines.size)
assertEquals(4, store.state.search.regionSearchEngines.size)
@@ -321,6 +327,9 @@ class SearchUseCasesTest {
SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM)
),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION)
+ ),
additionalSearchEngines = listOf(
SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL)
),
@@ -340,7 +349,7 @@ class SearchUseCasesTest {
val useCases = SearchUseCases(store, mock(), mock())
- assertEquals(6, store.state.search.searchEngines.size)
+ assertEquals(7, store.state.search.searchEngines.size)
assertEquals(3, store.state.search.availableSearchEngines.size)
useCases.addSearchEngine.invoke(
@@ -349,7 +358,7 @@ class SearchUseCasesTest {
store.waitUntilIdle()
- assertEquals(7, store.state.search.searchEngines.size)
+ assertEquals(8, store.state.search.searchEngines.size)
assertEquals(2, store.state.search.availableSearchEngines.size)
assertEquals(1, store.state.search.additionalAvailableSearchEngines.size)
@@ -374,6 +383,9 @@ class SearchUseCasesTest {
SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM)
),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION)
+ ),
additionalSearchEngines = listOf(
SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL)
),
@@ -393,7 +405,7 @@ class SearchUseCasesTest {
val useCases = SearchUseCases(store, mock(), mock())
- assertEquals(6, store.state.search.searchEngines.size)
+ assertEquals(7, store.state.search.searchEngines.size)
assertEquals(3, store.state.search.availableSearchEngines.size)
useCases.addSearchEngine.invoke(
@@ -406,7 +418,7 @@ class SearchUseCasesTest {
store.waitUntilIdle()
- assertEquals(7, store.state.search.searchEngines.size)
+ assertEquals(8, store.state.search.searchEngines.size)
assertEquals(3, store.state.search.availableSearchEngines.size)
assertEquals(3, store.state.search.customSearchEngines.size)
@@ -432,6 +444,9 @@ class SearchUseCasesTest {
SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM)
),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION)
+ ),
additionalSearchEngines = listOf(
SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL)
),
@@ -451,7 +466,7 @@ class SearchUseCasesTest {
val useCases = SearchUseCases(store, mock(), mock())
- assertEquals(6, store.state.search.searchEngines.size)
+ assertEquals(7, store.state.search.searchEngines.size)
assertEquals(3, store.state.search.availableSearchEngines.size)
useCases.removeSearchEngine.invoke(
@@ -460,7 +475,7 @@ class SearchUseCasesTest {
store.waitUntilIdle()
- assertEquals(5, store.state.search.searchEngines.size)
+ assertEquals(6, store.state.search.searchEngines.size)
assertEquals(4, store.state.search.availableSearchEngines.size)
assertEquals(2, store.state.search.regionSearchEngines.size)
@@ -485,6 +500,9 @@ class SearchUseCasesTest {
SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM)
),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION)
+ ),
additionalSearchEngines = listOf(
SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL)
),
@@ -504,7 +522,7 @@ class SearchUseCasesTest {
val useCases = SearchUseCases(store, mock(), mock())
- assertEquals(6, store.state.search.searchEngines.size)
+ assertEquals(7, store.state.search.searchEngines.size)
assertEquals(3, store.state.search.availableSearchEngines.size)
useCases.removeSearchEngine.invoke(
@@ -513,7 +531,7 @@ class SearchUseCasesTest {
store.waitUntilIdle()
- assertEquals(5, store.state.search.searchEngines.size)
+ assertEquals(6, store.state.search.searchEngines.size)
assertEquals(4, store.state.search.availableSearchEngines.size)
assertEquals(0, store.state.search.additionalSearchEngines.size)
@@ -538,6 +556,9 @@ class SearchUseCasesTest {
SearchEngine("engine-d", "Engine D", mock(), type = SearchEngine.Type.CUSTOM),
SearchEngine("engine-e", "Engine E", mock(), type = SearchEngine.Type.CUSTOM)
),
+ applicationSearchEngines = listOf(
+ SearchEngine("engine-j", "Engine J", mock(), type = SearchEngine.Type.APPLICATION)
+ ),
additionalSearchEngines = listOf(
SearchEngine("engine-f", "Engine F", mock(), type = SearchEngine.Type.BUNDLED_ADDITIONAL)
),
@@ -557,7 +578,7 @@ class SearchUseCasesTest {
val useCases = SearchUseCases(store, mock(), mock())
- assertEquals(6, store.state.search.searchEngines.size)
+ assertEquals(7, store.state.search.searchEngines.size)
assertEquals(3, store.state.search.availableSearchEngines.size)
useCases.removeSearchEngine.invoke(
@@ -566,7 +587,7 @@ class SearchUseCasesTest {
store.waitUntilIdle()
- assertEquals(5, store.state.search.searchEngines.size)
+ assertEquals(6, store.state.search.searchEngines.size)
assertEquals(3, store.state.search.availableSearchEngines.size)
assertEquals(1, store.state.search.customSearchEngines.size)
diff --git a/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/SearchMiddlewareTest.kt b/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/SearchMiddlewareTest.kt
index bcfaa949ceb..e17d01298d2 100644
--- a/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/SearchMiddlewareTest.kt
+++ b/components/feature/search/src/test/java/mozilla/components/feature/search/middleware/SearchMiddlewareTest.kt
@@ -5,10 +5,7 @@
package mozilla.components.feature.search.middleware
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.TestDispatcher
import mozilla.components.browser.state.action.SearchAction
import mozilla.components.browser.state.search.RegionState
import mozilla.components.browser.state.search.SearchEngine
@@ -24,12 +21,15 @@ import mozilla.components.support.test.fakes.android.FakeSharedPreferences
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.doReturn
@@ -40,21 +40,19 @@ import java.util.UUID
@RunWith(AndroidJUnit4::class)
class SearchMiddlewareTest {
- private lateinit var dispatcher: TestCoroutineDispatcher
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
private lateinit var originalLocale: Locale
- private lateinit var scope: CoroutineScope
@Before
fun setUp() {
- dispatcher = TestCoroutineDispatcher()
- scope = CoroutineScope(dispatcher)
originalLocale = Locale.getDefault()
}
@After
fun tearDown() {
- dispatcher.cleanupTestCoroutines()
- scope.cancel()
if (Locale.getDefault() != originalLocale) {
Locale.setDefault(originalLocale)
@@ -752,9 +750,9 @@ class SearchMiddlewareTest {
}
@Test
- fun `Loads additional search engine and honors user choice`() {
+ fun `Loads additional search engine and honors user choice`() = runTestOnMain {
val metadataStorage = SearchMetadataStorage(testContext, lazy { FakeSharedPreferences() })
- runBlocking { metadataStorage.setAdditionalSearchEngines(listOf("reddit")) }
+ metadataStorage.setAdditionalSearchEngines(listOf("reddit"))
val searchMiddleware = SearchMiddleware(
testContext,
@@ -801,7 +799,7 @@ class SearchMiddlewareTest {
}
@Test
- fun `Loads custom search engines`() {
+ fun `Loads custom search engines`() = runTestOnMain {
val searchEngine = SearchEngine(
id = "test-search",
name = "Test Engine",
@@ -812,7 +810,7 @@ class SearchMiddlewareTest {
)
val storage = CustomSearchEngineStorage(testContext, dispatcher)
- runBlocking { storage.saveSearchEngine(searchEngine) }
+ storage.saveSearchEngine(searchEngine)
val store = BrowserStore(
middleware = listOf(
@@ -835,9 +833,9 @@ class SearchMiddlewareTest {
}
@Test
- fun `Loads default search engine ID`() {
+ fun `Loads default search engine ID`() = runTestOnMain {
val storage = SearchMetadataStorage(testContext)
- runBlocking { storage.setUserSelectedSearchEngine("test-id", null) }
+ storage.setUserSelectedSearchEngine("test-id", null)
val middleware = SearchMiddleware(
testContext,
@@ -1073,7 +1071,7 @@ class SearchMiddlewareTest {
@Test
fun `Custom search engines - Create, Update, Delete`() {
- runBlocking {
+ runTestOnMain {
val storage: SearchMiddleware.CustomStorage = mock()
doReturn(emptyList()).`when`(storage).loadSearchEngineList()
@@ -1584,12 +1582,12 @@ class SearchMiddlewareTest {
}
}
-private fun wait(store: BrowserStore, dispatcher: TestCoroutineDispatcher) {
+private fun wait(store: BrowserStore, dispatcher: TestDispatcher) {
// First we wait for the InitAction that may still need to be processed.
store.waitUntilIdle()
// Now we wait for the Middleware that may need to asynchronously process an action the test dispatched
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
// Since the Middleware may have dispatched an action, we now wait for the store again.
store.waitUntilIdle()
diff --git a/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionManagerTest.kt b/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionManagerTest.kt
index 34b4345437b..3eb227c53d6 100644
--- a/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionManagerTest.kt
+++ b/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionManagerTest.kt
@@ -4,7 +4,7 @@
package mozilla.components.feature.search.region
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.service.location.LocationService
import mozilla.components.support.test.fakes.FakeClock
import mozilla.components.support.test.fakes.android.FakeContext
@@ -28,7 +28,7 @@ class RegionManagerTest {
}
@Test
- fun `First update`() {
+ fun `First update`() = runTest {
val locationService = FakeLocationService(
region = LocationService.Region("DE", "Germany")
)
@@ -40,14 +40,14 @@ class RegionManagerTest {
preferences = lazy { FakeSharedPreferences() }
)
- val updatedRegion = runBlocking { regionManager.update() }
+ val updatedRegion = regionManager.update()
assertNotNull(updatedRegion!!)
assertEquals("DE", updatedRegion.current)
assertEquals("DE", updatedRegion.home)
}
@Test
- fun `Updating to new home region`() {
+ fun `Updating to new home region`() = runTest {
val clock = FakeClock()
val locationService = FakeLocationService(
@@ -61,12 +61,12 @@ class RegionManagerTest {
preferences = lazy { FakeSharedPreferences() }
)
- runBlocking { regionManager.update() }
+ regionManager.update()
locationService.region = LocationService.Region("FR", "France")
// Should not be updated since the "home" region didn't change
- assertNull(runBlocking { regionManager.update() })
+ assertNull(regionManager.update())
assertEquals("DE", regionManager.region()?.home)
assertEquals("FR", regionManager.region()?.current)
@@ -74,14 +74,14 @@ class RegionManagerTest {
clock.advanceBy(60L * 60L * 24L * 7L * 1000L)
// Still not updated because we switch after two weeks
- assertNull(runBlocking { regionManager.update() })
+ assertNull(regionManager.update())
assertEquals("DE", regionManager.region()?.home)
assertEquals("FR", regionManager.region()?.current)
// Let's move the clock 8 more days into the future
clock.advanceBy(60L * 60L * 24L * 8L * 1000L)
- val updatedRegion = (runBlocking { regionManager.update() })
+ val updatedRegion = (regionManager.update())
assertNotNull(updatedRegion!!)
assertEquals("FR", updatedRegion.home)
assertEquals("FR", updatedRegion.current)
@@ -90,7 +90,7 @@ class RegionManagerTest {
}
@Test
- fun `Switching back to home region after staying in different region shortly`() {
+ fun `Switching back to home region after staying in different region shortly`() = runTest {
val clock = FakeClock()
val locationService = FakeLocationService(
@@ -104,7 +104,7 @@ class RegionManagerTest {
preferences = lazy { FakeSharedPreferences() }
)
- runBlocking { regionManager.update() }
+ regionManager.update()
// Let's jump one week into the future!
clock.advanceBy(60L * 60L * 24L * 7L * 1000L)
@@ -112,7 +112,7 @@ class RegionManagerTest {
locationService.region = LocationService.Region("FR", "France")
// Should not be updated since the "home" region didn't change
- assertNull(runBlocking { regionManager.update() })
+ assertNull(regionManager.update())
assertEquals("DE", regionManager.region()?.home)
assertEquals("FR", regionManager.region()?.current)
@@ -120,7 +120,7 @@ class RegionManagerTest {
clock.advanceBy(60L * 60L * 24L * 1000L)
locationService.region = LocationService.Region("DE", "Germany")
- assertNull(runBlocking { regionManager.update() })
+ assertNull(regionManager.update())
assertEquals("DE", regionManager.region()?.home)
assertEquals("DE", regionManager.region()?.current)
@@ -131,7 +131,7 @@ class RegionManagerTest {
// The "home" region should not have changed since we haven't been in the other region the
// whole time.
- assertNull(runBlocking { regionManager.update() })
+ assertNull(regionManager.update())
assertEquals("DE", regionManager.region()?.home)
assertEquals("FR", regionManager.region()?.current)
}
diff --git a/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionMiddlewareTest.kt b/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionMiddlewareTest.kt
index 177d0e4d09f..9cc22a78978 100644
--- a/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionMiddlewareTest.kt
+++ b/components/feature/search/src/test/java/mozilla/components/feature/search/region/RegionMiddlewareTest.kt
@@ -4,8 +4,6 @@
package mozilla.components.feature.search.region
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.browser.state.action.InitAction
import mozilla.components.browser.state.search.RegionState
import mozilla.components.browser.state.store.BrowserStore
@@ -15,14 +13,20 @@ import mozilla.components.support.test.fakes.FakeClock
import mozilla.components.support.test.fakes.android.FakeContext
import mozilla.components.support.test.fakes.android.FakeSharedPreferences
import mozilla.components.support.test.libstate.ext.waitUntilIdle
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
class RegionMiddlewareTest {
- private lateinit var dispatcher: TestCoroutineDispatcher
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
+
private lateinit var locationService: FakeLocationService
private lateinit var clock: FakeClock
private lateinit var regionManager: RegionManager
@@ -30,7 +34,6 @@ class RegionMiddlewareTest {
@Before
fun setUp() {
clock = FakeClock()
- dispatcher = TestCoroutineDispatcher()
locationService = FakeLocationService()
regionManager = RegionManager(
context = FakeContext(),
@@ -40,11 +43,6 @@ class RegionMiddlewareTest {
)
}
- @After
- fun tearDown() {
- dispatcher.cleanupTestCoroutines()
- }
-
@Test
fun `Updates region on init`() {
val middleware = RegionMiddleware(FakeContext(), locationService, dispatcher)
@@ -76,7 +74,7 @@ class RegionMiddlewareTest {
store.dispatch(InitAction).joinBlocking()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
assertEquals(RegionState.Default, store.state.search.region)
@@ -85,12 +83,12 @@ class RegionMiddlewareTest {
}
@Test
- fun `Dispatches cached home region and update later`() {
+ fun `Dispatches cached home region and update later`() = runTestOnMain {
val middleware = RegionMiddleware(FakeContext(), locationService, dispatcher)
middleware.regionManager = regionManager
locationService.region = LocationService.Region("FR", "France")
- runBlocking { regionManager.update() }
+ regionManager.update()
val store = BrowserStore(
middleware = listOf(middleware)
@@ -104,7 +102,7 @@ class RegionMiddlewareTest {
assertEquals("FR", store.state.search.region!!.current)
locationService.region = LocationService.Region("DE", "Germany")
- runBlocking { regionManager.update() }
+ regionManager.update()
store.dispatch(InitAction).joinBlocking()
middleware.updateJob?.joinBlocking()
diff --git a/components/feature/search/src/test/java/mozilla/components/feature/search/storage/BundledSearchEnginesStorageTest.kt b/components/feature/search/src/test/java/mozilla/components/feature/search/storage/BundledSearchEnginesStorageTest.kt
index e56ec6d4bc4..ec766fb7bf1 100644
--- a/components/feature/search/src/test/java/mozilla/components/feature/search/storage/BundledSearchEnginesStorageTest.kt
+++ b/components/feature/search/src/test/java/mozilla/components/feature/search/storage/BundledSearchEnginesStorageTest.kt
@@ -5,7 +5,7 @@
package mozilla.components.feature.search.storage
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.search.RegionState
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.support.test.robolectric.testContext
@@ -20,7 +20,7 @@ import java.util.Locale
@RunWith(AndroidJUnit4::class)
class BundledSearchEnginesStorageTest {
@Test
- fun `Load search engines for en-US from assets`() = runBlocking {
+ fun `Load search engines for en-US from assets`() = runTest {
val storage = BundledSearchEnginesStorage(testContext)
val engines = storage.load(RegionState("US", "US"), Locale("en", "US"))
@@ -30,7 +30,7 @@ class BundledSearchEnginesStorageTest {
}
@Test
- fun `Load search engines for all known locales without region`() = runBlocking {
+ fun `Load search engines for all known locales without region`() = runTest {
val storage = BundledSearchEnginesStorage(testContext)
val locales = Locale.getAvailableLocales()
assertTrue(locales.isNotEmpty())
@@ -43,7 +43,7 @@ class BundledSearchEnginesStorageTest {
}
@Test
- fun `Load search engines for de-DE with global US region override`() = runBlocking {
+ fun `Load search engines for de-DE with global US region override`() = runTest {
// Without region
run {
val storage = BundledSearchEnginesStorage(testContext)
@@ -67,7 +67,7 @@ class BundledSearchEnginesStorageTest {
}
@Test
- fun `Load search engines for en-US with local RU region override`() = runBlocking {
+ fun `Load search engines for en-US with local RU region override`() = runTest {
// Without region
run {
val storage = BundledSearchEnginesStorage(testContext)
@@ -92,7 +92,7 @@ class BundledSearchEnginesStorageTest {
}
@Test
- fun `Load search engines for zh-CN_CN locale with searchDefault override`() = runBlocking {
+ fun `Load search engines for zh-CN_CN locale with searchDefault override`() = runTest {
val storage = BundledSearchEnginesStorage(testContext)
val engines = storage.load(RegionState("CN", "CN"), Locale("zh", "CN"))
val searchEngines = engines.list
@@ -112,7 +112,7 @@ class BundledSearchEnginesStorageTest {
}
@Test
- fun `Load search engines for ru_RU locale with engines not in searchOrder`() = runBlocking {
+ fun `Load search engines for ru_RU locale with engines not in searchOrder`() = runTest {
val storage = BundledSearchEnginesStorage(testContext)
val engines = storage.load(RegionState("RU", "RU"), Locale("ru", "RU"))
val searchEngines = engines.list
@@ -129,7 +129,7 @@ class BundledSearchEnginesStorageTest {
}
@Test
- fun `Load search engines for trs locale with non-google initial engines and no default`() = runBlocking {
+ fun `Load search engines for trs locale with non-google initial engines and no default`() = runTest {
val storage = BundledSearchEnginesStorage(testContext)
val engines = storage.load(RegionState.Default, Locale("trs", ""))
val searchEngines = engines.list
@@ -149,7 +149,7 @@ class BundledSearchEnginesStorageTest {
}
@Test
- fun `Load search engines for locale not in configuration`() = runBlocking {
+ fun `Load search engines for locale not in configuration`() = runTest {
val storage = BundledSearchEnginesStorage(testContext)
val engines = storage.load(RegionState.Default, Locale("xx", "XX"))
val searchEngines = engines.list
@@ -175,7 +175,7 @@ class BundledSearchEnginesStorageTest {
}
@Test
- fun `Verify values of Google search engine`() = runBlocking {
+ fun `Verify values of Google search engine`() = runTest {
val storage = BundledSearchEnginesStorage(testContext)
val engines = storage.load(RegionState("US", "US"), Locale("en", "US"))
diff --git a/components/feature/search/src/test/java/mozilla/components/feature/search/storage/CustomSearchEngineStorageTest.kt b/components/feature/search/src/test/java/mozilla/components/feature/search/storage/CustomSearchEngineStorageTest.kt
index a063830ac30..2feee8eecc2 100644
--- a/components/feature/search/src/test/java/mozilla/components/feature/search/storage/CustomSearchEngineStorageTest.kt
+++ b/components/feature/search/src/test/java/mozilla/components/feature/search/storage/CustomSearchEngineStorageTest.kt
@@ -5,7 +5,7 @@
package mozilla.components.feature.search.storage
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
@@ -18,7 +18,7 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CustomSearchEngineStorageTest {
@Test
- fun `saveSearchEngine successfully saves`() = runBlockingTest {
+ fun `saveSearchEngine successfully saves`() = runTest {
val searchEngine = SearchEngine(
id = "id1",
name = "example",
@@ -33,7 +33,7 @@ class CustomSearchEngineStorageTest {
}
@Test
- fun `loadSearchEngine successfully loads after saving`() = runBlockingTest {
+ fun `loadSearchEngine successfully loads after saving`() = runTest {
val searchEngine = SearchEngine(
id = "id1",
name = "example",
@@ -54,7 +54,7 @@ class CustomSearchEngineStorageTest {
@Test
@Ignore("https://github.com/mozilla-mobile/android-components/issues/8124")
- fun `loadSearchEngineList successfully loads after saving`() = runBlockingTest {
+ fun `loadSearchEngineList successfully loads after saving`() = runTest {
val searchEngine = SearchEngine(
id = "id1",
name = "example",
@@ -87,7 +87,7 @@ class CustomSearchEngineStorageTest {
}
@Test
- fun `removeSearchEngine successfully deletes`() = runBlockingTest {
+ fun `removeSearchEngine successfully deletes`() = runTest {
val searchEngine = SearchEngine(
id = "id1",
name = "example",
diff --git a/components/feature/search/src/test/java/mozilla/components/feature/search/suggestions/SearchSuggestionClientTest.kt b/components/feature/search/src/test/java/mozilla/components/feature/search/suggestions/SearchSuggestionClientTest.kt
index ed9678908f0..0586aad4d47 100644
--- a/components/feature/search/src/test/java/mozilla/components/feature/search/suggestions/SearchSuggestionClientTest.kt
+++ b/components/feature/search/src/test/java/mozilla/components/feature/search/suggestions/SearchSuggestionClientTest.kt
@@ -5,7 +5,7 @@
package mozilla.components.feature.search.suggestions
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.SearchState
import mozilla.components.browser.state.store.BrowserStore
@@ -33,19 +33,17 @@ class SearchSuggestionClientTest {
)
@Test
- fun `Get a list of results based on the Google search engine`() {
+ fun `Get a list of results based on the Google search engine`() = runTest {
val client = SearchSuggestionClient(searchEngine, GOOGLE_MOCK_RESPONSE)
+ val expectedResults = listOf("firefox", "firefox for mac", "firefox quantum", "firefox update", "firefox esr", "firefox focus", "firefox addons", "firefox extensions", "firefox nightly", "firefox clear cache")
- runBlocking {
- val results = client.getSuggestions("firefox")
- val expectedResults = listOf("firefox", "firefox for mac", "firefox quantum", "firefox update", "firefox esr", "firefox focus", "firefox addons", "firefox extensions", "firefox nightly", "firefox clear cache")
+ val results = client.getSuggestions("firefox")
- assertEquals(expectedResults, results)
- }
+ assertEquals(expectedResults, results)
}
@Test
- fun `Get a list of results based on a non google search engine`() {
+ fun `Get a list of results based on a non google search engine`() = runTest {
val qwant = createSearchEngine(
name = "Qwant",
url = "https://localhost?q={searchTerms}",
@@ -53,51 +51,43 @@ class SearchSuggestionClientTest {
icon = mock()
)
val client = SearchSuggestionClient(qwant, QWANT_MOCK_RESPONSE)
+ val expectedResults = listOf("firefox (video game)", "firefox addons", "firefox", "firefox quantum", "firefox focus")
- runBlocking {
- val results = client.getSuggestions("firefox")
- val expectedResults = listOf("firefox (video game)", "firefox addons", "firefox", "firefox quantum", "firefox focus")
+ val results = client.getSuggestions("firefox")
- assertEquals(expectedResults, results)
- }
+ assertEquals(expectedResults, results)
}
@Test(expected = SearchSuggestionClient.ResponseParserException::class)
- fun `Check that a bad response will throw a parser exception`() {
+ fun `Check that a bad response will throw a parser exception`() = runTest {
val client = SearchSuggestionClient(searchEngine, SERVER_ERROR_RESPONSE)
- runBlocking {
- client.getSuggestions("firefox")
- }
+ client.getSuggestions("firefox")
}
@Test(expected = SearchSuggestionClient.FetchException::class)
- fun `Check that an exception in the suggestionFetcher will re-throw an IOException`() {
+ fun `Check that an exception in the suggestionFetcher will re-throw an IOException`() = runTest {
val client = SearchSuggestionClient(searchEngine) { throw IOException() }
- runBlocking {
- client.getSuggestions("firefox")
- }
+ client.getSuggestions("firefox")
}
@Test
- fun `Check that a search engine without a suggestURI will return an empty suggestion list`() {
+ fun `Check that a search engine without a suggestURI will return an empty suggestion list`() = runTest {
val searchEngine = createSearchEngine(
name = "Test",
url = "https://localhost?q={searchTerms}",
icon = mock()
)
-
val client = SearchSuggestionClient(searchEngine) { "no-op" }
- runBlocking {
- val results = client.getSuggestions("firefox")
- assertEquals(emptyList(), results)
- }
+ val results = client.getSuggestions("firefox")
+
+ assertEquals(emptyList(), results)
}
@Test
- fun `Default search engine is used if search engine manager provided`() {
+ fun `Default search engine is used if search engine manager provided`() = runTest {
val store = BrowserStore(
BrowserState(
search = SearchState(
@@ -111,12 +101,10 @@ class SearchSuggestionClientTest {
store,
GOOGLE_MOCK_RESPONSE
)
+ val expectedResults = listOf("firefox", "firefox for mac", "firefox quantum", "firefox update", "firefox esr", "firefox focus", "firefox addons", "firefox extensions", "firefox nightly", "firefox clear cache")
- runBlocking {
- val results = client.getSuggestions("firefox")
- val expectedResults = listOf("firefox", "firefox for mac", "firefox quantum", "firefox update", "firefox esr", "firefox focus", "firefox addons", "firefox extensions", "firefox nightly", "firefox clear cache")
+ val results = client.getSuggestions("firefox")
- assertEquals(expectedResults, results)
- }
+ assertEquals(expectedResults, results)
}
}
diff --git a/components/feature/session/src/test/java/mozilla/components/feature/session/FullScreenFeatureTest.kt b/components/feature/session/src/test/java/mozilla/components/feature/session/FullScreenFeatureTest.kt
index b7a368b0e48..43f69071e49 100644
--- a/components/feature/session/src/test/java/mozilla/components/feature/session/FullScreenFeatureTest.kt
+++ b/components/feature/session/src/test/java/mozilla/components/feature/session/FullScreenFeatureTest.kt
@@ -5,11 +5,6 @@
package mozilla.components.feature.session
import android.view.WindowManager
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
@@ -18,13 +13,13 @@ import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
-import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mockito.doReturn
@@ -32,20 +27,9 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.verify
class FullScreenFeatureTest {
- private val testDispatcher = TestCoroutineDispatcher()
- @Before
- @ExperimentalCoroutinesApi
- fun setUp() {
- Dispatchers.setMain(testDispatcher)
- }
-
- @After
- @ExperimentalCoroutinesApi
- fun tearDown() {
- Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
- }
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
@Test
fun `Starting without tabs`() {
@@ -62,8 +46,6 @@ class FullScreenFeatureTest {
)
feature.start()
-
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
assertNull(viewPort)
@@ -91,8 +73,6 @@ class FullScreenFeatureTest {
)
feature.start()
-
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
assertNull(viewPort)
@@ -134,8 +114,6 @@ class FullScreenFeatureTest {
)
feature.start()
-
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
assertEquals(42, viewPort)
diff --git a/components/feature/session/src/test/java/mozilla/components/feature/session/HistoryDelegateTest.kt b/components/feature/session/src/test/java/mozilla/components/feature/session/HistoryDelegateTest.kt
index 9d2d584492e..c0687803833 100644
--- a/components/feature/session/src/test/java/mozilla/components/feature/session/HistoryDelegateTest.kt
+++ b/components/feature/session/src/test/java/mozilla/components/feature/session/HistoryDelegateTest.kt
@@ -5,7 +5,7 @@
package mozilla.components.feature.session
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.storage.FrecencyThresholdOption
import mozilla.components.concept.storage.HistoryAutocompleteResult
import mozilla.components.concept.storage.HistoryStorage
@@ -28,7 +28,7 @@ import org.mockito.Mockito.verify
class HistoryDelegateTest {
@Test
- fun `history delegate passes through onVisited calls`() = runBlocking {
+ fun `history delegate passes through onVisited calls`() = runTest {
val storage = mock()
val delegate = HistoryDelegate(lazy { storage })
@@ -43,7 +43,7 @@ class HistoryDelegateTest {
}
@Test
- fun `history delegate passes through onTitleChanged calls`() = runBlocking {
+ fun `history delegate passes through onTitleChanged calls`() = runTest {
val storage = mock()
val delegate = HistoryDelegate(lazy { storage })
@@ -52,7 +52,7 @@ class HistoryDelegateTest {
}
@Test
- fun `history delegate passes through onPreviewImageChange calls`() = runBlocking {
+ fun `history delegate passes through onPreviewImageChange calls`() = runTest {
val storage = mock()
val delegate = HistoryDelegate(lazy { storage })
@@ -65,7 +65,7 @@ class HistoryDelegateTest {
}
@Test
- fun `history delegate passes through getVisited calls`() = runBlocking {
+ fun `history delegate passes through getVisited calls`() = runTest {
val storage = TestHistoryStorage()
val delegate = HistoryDelegate(lazy { storage })
@@ -84,7 +84,7 @@ class HistoryDelegateTest {
}
@Test
- fun `history delegate checks with storage canAddUriCalled`() = runBlocking {
+ fun `history delegate checks with storage canAddUriCalled`() = runTest {
val storage = TestHistoryStorage()
val delegate = HistoryDelegate(lazy { storage })
diff --git a/components/feature/session/src/test/java/mozilla/components/feature/session/SessionFeatureTest.kt b/components/feature/session/src/test/java/mozilla/components/feature/session/SessionFeatureTest.kt
index 0122d10d5da..f3970b2df6e 100644
--- a/components/feature/session/src/test/java/mozilla/components/feature/session/SessionFeatureTest.kt
+++ b/components/feature/session/src/test/java/mozilla/components/feature/session/SessionFeatureTest.kt
@@ -5,7 +5,6 @@
package mozilla.components.feature.session
import android.view.View
-import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.CrashAction
@@ -42,8 +41,7 @@ class SessionFeatureTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val scope = TestCoroutineScope(coroutinesTestRule.testDispatcher)
- private val testDispatcher = coroutinesTestRule.testDispatcher
+ private val scope = coroutinesTestRule.scope
@Test
fun `start renders selected session`() {
@@ -60,7 +58,6 @@ class SessionFeatureTest {
verify(view, never()).render(any())
feature.start()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
verify(view).render(engineSession)
@@ -82,7 +79,6 @@ class SessionFeatureTest {
feature.start()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
verify(view).render(engineSession)
@@ -103,7 +99,6 @@ class SessionFeatureTest {
verify(view, never()).render(any())
feature.start()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
verify(view).render(engineSession)
@@ -126,12 +121,10 @@ class SessionFeatureTest {
verify(view, never()).render(any())
feature.start()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
verify(view).render(engineSessionB)
store.dispatch(TabListAction.SelectTabAction("A")).joinBlocking()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
verify(view).render(engineSessionA)
}
@@ -147,7 +140,6 @@ class SessionFeatureTest {
verify(view, never()).render(any())
feature.start()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
verify(store).dispatch(EngineAction.CreateEngineSessionAction("B"))
}
@@ -169,14 +161,12 @@ class SessionFeatureTest {
verify(view, never()).render(any())
feature.start()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
verify(view).render(engineSessionB)
feature.stop()
store.dispatch(TabListAction.SelectTabAction("A")).joinBlocking()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
verify(view, never()).render(engineSessionA)
}
@@ -195,7 +185,6 @@ class SessionFeatureTest {
feature.start()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
verify(view).render(engineSession)
@@ -221,7 +210,6 @@ class SessionFeatureTest {
feature.start()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
verify(view).render(engineSession)
@@ -250,7 +238,6 @@ class SessionFeatureTest {
feature.start()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
verify(view).render(engineSession)
@@ -340,7 +327,6 @@ class SessionFeatureTest {
verify(view, never()).render(any())
feature.start()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
verify(view).render(engineSession)
@@ -366,7 +352,6 @@ class SessionFeatureTest {
feature.start()
store.dispatch(CrashAction.SessionCrashedAction("A")).joinBlocking()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
verify(view, atLeastOnce()).release()
middleware.assertNotDispatched(EngineAction.CreateEngineSessionAction::class)
@@ -388,7 +373,6 @@ class SessionFeatureTest {
assertEquals(0L, store.state.findTab("B")?.lastAccess)
feature.start()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
assertNotEquals(0L, store.state.findTab("B")?.lastAccess)
diff --git a/components/feature/session/src/test/java/mozilla/components/feature/session/SessionUseCasesTest.kt b/components/feature/session/src/test/java/mozilla/components/feature/session/SessionUseCasesTest.kt
index d73e3fc0f2f..e34c936a73a 100644
--- a/components/feature/session/src/test/java/mozilla/components/feature/session/SessionUseCasesTest.kt
+++ b/components/feature/session/src/test/java/mozilla/components/feature/session/SessionUseCasesTest.kt
@@ -4,7 +4,7 @@
package mozilla.components.feature.session
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.CrashAction
import mozilla.components.browser.state.action.EngineAction
@@ -249,7 +249,7 @@ class SessionUseCasesTest {
}
@Test
- fun stopLoading() = runBlocking {
+ fun stopLoading() = runTest {
useCases.stopLoading()
store.waitUntilIdle()
verify(engineSession).stopLoading()
diff --git a/components/feature/session/src/test/java/mozilla/components/feature/session/SwipeRefreshFeatureTest.kt b/components/feature/session/src/test/java/mozilla/components/feature/session/SwipeRefreshFeatureTest.kt
index 18a49c4e1b6..384e3c0793b 100644
--- a/components/feature/session/src/test/java/mozilla/components/feature/session/SwipeRefreshFeatureTest.kt
+++ b/components/feature/session/src/test/java/mozilla/components/feature/session/SwipeRefreshFeatureTest.kt
@@ -9,11 +9,6 @@ import android.graphics.Bitmap
import android.widget.FrameLayout
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.state.BrowserState
@@ -26,10 +21,11 @@ import mozilla.components.concept.engine.selection.SelectionActionDelegate
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.doReturn
@@ -39,8 +35,9 @@ import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class SwipeRefreshFeatureTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
- private val testDispatcher = TestCoroutineDispatcher()
private lateinit var store: BrowserStore
private lateinit var refreshFeature: SwipeRefreshFeature
private val mockLayout = mock()
@@ -48,7 +45,6 @@ class SwipeRefreshFeatureTest {
@Before
fun setup() {
- Dispatchers.setMain(testDispatcher)
store = BrowserStore(
BrowserState(
tabs = listOf(
@@ -62,13 +58,6 @@ class SwipeRefreshFeatureTest {
refreshFeature = SwipeRefreshFeature(store, useCase, mockLayout)
}
- @After
- @ExperimentalCoroutinesApi
- fun tearDown() {
- Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
- }
-
@Test
fun `sets the onRefreshListener and onChildScrollUpCallback`() {
verify(mockLayout).setOnRefreshListener(refreshFeature)
@@ -103,7 +92,6 @@ class SwipeRefreshFeatureTest {
val selectedTab = store.state.findCustomTabOrSelectedTab()!!
store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(selectedTab.id, true)).joinBlocking()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
assertFalse(selectedTab.content.refreshCanceled)
@@ -112,7 +100,6 @@ class SwipeRefreshFeatureTest {
@Test
fun `feature clears the swipeRefreshLayout#isRefreshing when tab fishes loading or a refreshCanceled`() {
refreshFeature.start()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
val selectedTab = store.state.findCustomTabOrSelectedTab()!!
@@ -121,7 +108,6 @@ class SwipeRefreshFeatureTest {
reset(mockLayout)
store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(selectedTab.id, true)).joinBlocking()
- testDispatcher.advanceUntilIdle()
store.waitUntilIdle()
verify(mockLayout, times(2)).isRefreshing = false
@@ -130,7 +116,6 @@ class SwipeRefreshFeatureTest {
// As if we dispatch with loading = false, none event will be trigger.
store.dispatch(ContentAction.UpdateLoadingStateAction(selectedTab.id, true)).joinBlocking()
store.dispatch(ContentAction.UpdateLoadingStateAction(selectedTab.id, false)).joinBlocking()
- testDispatcher.advanceUntilIdle()
verify(mockLayout, times(3)).isRefreshing = false
}
diff --git a/components/feature/session/src/test/java/mozilla/components/feature/session/middleware/undo/UndoMiddlewareTest.kt b/components/feature/session/src/test/java/mozilla/components/feature/session/middleware/undo/UndoMiddlewareTest.kt
index 318f4b770b8..495372ff13b 100644
--- a/components/feature/session/src/test/java/mozilla/components/feature/session/middleware/undo/UndoMiddlewareTest.kt
+++ b/components/feature/session/src/test/java/mozilla/components/feature/session/middleware/undo/UndoMiddlewareTest.kt
@@ -4,11 +4,8 @@
package mozilla.components.feature.session.middleware.undo
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.withContext
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.action.UndoAction
import mozilla.components.browser.state.selector.selectedTab
@@ -17,30 +14,21 @@ import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
-import org.junit.Before
+import org.junit.Rule
import org.junit.Test
class UndoMiddlewareTest {
- private lateinit var testDispatcher: TestCoroutineDispatcher
-
- @Before
- fun setUp() {
- testDispatcher = TestCoroutineDispatcher()
- Dispatchers.setMain(testDispatcher)
- }
-
- @After
- fun tearDown() {
- Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
- }
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
@Test
- fun `Undo scenario - Removing single tab`() {
+ fun `Undo scenario - Removing single tab`() = runTestOnMain {
val store = BrowserStore(
middleware = listOf(
UndoMiddleware(clearAfterMillis = 60000)
@@ -65,22 +53,14 @@ class UndoMiddlewareTest {
assertEquals(1, store.state.tabs.size)
assertEquals("https://getpocket.com", store.state.selectedTab!!.content.url)
- testDispatcher.withDispatchingPaused {
- // We need to pause the test dispatcher here to avoid it dispatching immediately.
- // Otherwise we deadlock the test here when we wait for the store to complete and
- // at the same time the middleware dispatches a coroutine on the dispatcher which will
- // also block on the store in SessionManager.restore().
- store.dispatch(UndoAction.RestoreRecoverableTabs).joinBlocking()
- }
-
- store.waitUntilIdle()
+ restoreRecoverableTabs(dispatcher, store)
assertEquals(2, store.state.tabs.size)
assertEquals("https://www.mozilla.org", store.state.selectedTab!!.content.url)
}
@Test
- fun `Undo scenario - Removing list of tabs`() {
+ fun `Undo scenario - Removing list of tabs`() = runTestOnMain {
val store = BrowserStore(
middleware = listOf(
UndoMiddleware(clearAfterMillis = 60000)
@@ -105,22 +85,14 @@ class UndoMiddlewareTest {
assertEquals(1, store.state.tabs.size)
assertEquals("https://firefox.com", store.state.selectedTab!!.content.url)
- testDispatcher.withDispatchingPaused {
- // We need to pause the test dispatcher here to avoid it dispatching immediately.
- // Otherwise we deadlock the test here when we wait for the store to complete and
- // at the same time the middleware dispatches a coroutine on the dispatcher which will
- // also block on the store in SessionManager.restore().
- store.dispatch(UndoAction.RestoreRecoverableTabs).joinBlocking()
- }
-
- store.waitUntilIdle()
+ restoreRecoverableTabs(dispatcher, store)
assertEquals(3, store.state.tabs.size)
assertEquals("https://www.mozilla.org", store.state.selectedTab!!.content.url)
}
@Test
- fun `Undo scenario - Removing all normal tabs`() {
+ fun `Undo scenario - Removing all normal tabs`() = runTestOnMain {
val store = BrowserStore(
middleware = listOf(
UndoMiddleware(clearAfterMillis = 60000)
@@ -145,22 +117,14 @@ class UndoMiddlewareTest {
assertEquals(1, store.state.tabs.size)
assertNull(store.state.selectedTab)
- testDispatcher.withDispatchingPaused {
- // We need to pause the test dispatcher here to avoid it dispatching immediately.
- // Otherwise we deadlock the test here when we wait for the store to complete and
- // at the same time the middleware dispatches a coroutine on the dispatcher which will
- // also block on the store in SessionManager.restore().
- store.dispatch(UndoAction.RestoreRecoverableTabs).joinBlocking()
- }
-
- store.waitUntilIdle()
+ restoreRecoverableTabs(dispatcher, store)
assertEquals(3, store.state.tabs.size)
assertEquals("https://getpocket.com", store.state.selectedTab!!.content.url)
}
@Test
- fun `Undo scenario - Removing all tabs`() {
+ fun `Undo scenario - Removing all tabs`() = runTestOnMain {
val store = BrowserStore(
middleware = listOf(
UndoMiddleware(clearAfterMillis = 60000)
@@ -185,22 +149,14 @@ class UndoMiddlewareTest {
assertEquals(0, store.state.tabs.size)
assertNull(store.state.selectedTab)
- testDispatcher.withDispatchingPaused {
- // We need to pause the test dispatcher here to avoid it dispatching immediately.
- // Otherwise we deadlock the test here when we wait for the store to complete and
- // at the same time the middleware dispatches a coroutine on the dispatcher which will
- // also block on the store in SessionManager.restore().
- store.dispatch(UndoAction.RestoreRecoverableTabs).joinBlocking()
- }
-
- store.waitUntilIdle()
+ restoreRecoverableTabs(dispatcher, store)
assertEquals(3, store.state.tabs.size)
assertEquals("https://getpocket.com", store.state.selectedTab!!.content.url)
}
@Test
- fun `Undo scenario - Removing all tabs non-recoverable`() {
+ fun `Undo scenario - Removing all tabs non-recoverable`() = runTestOnMain {
val store = BrowserStore(
middleware = listOf(
UndoMiddleware(clearAfterMillis = 60000)
@@ -225,13 +181,7 @@ class UndoMiddlewareTest {
assertEquals(0, store.state.tabs.size)
assertNull(store.state.selectedTab)
- testDispatcher.withDispatchingPaused {
- // We need to pause the test dispatcher here to avoid it dispatching immediately.
- // Otherwise we deadlock the test here when we wait for the store to complete and
- // at the same time the middleware dispatches a coroutine on the dispatcher which will
- // also block on the store in SessionManager.restore().
- store.dispatch(UndoAction.RestoreRecoverableTabs).joinBlocking()
- }
+ restoreRecoverableTabs(dispatcher, store)
store.waitUntilIdle()
@@ -239,7 +189,7 @@ class UndoMiddlewareTest {
}
@Test
- fun `Undo History in State is written`() {
+ fun `Undo History in State is written`() = runTestOnMain {
val store = BrowserStore(
middleware = listOf(
UndoMiddleware(clearAfterMillis = 60000)
@@ -277,15 +227,7 @@ class UndoMiddlewareTest {
assertEquals("https://getpocket.com", store.state.undoHistory.tabs[1].state.url)
assertEquals(0, store.state.tabs.size)
- testDispatcher.withDispatchingPaused {
- // We need to pause the test dispatcher here to avoid it dispatching immediately.
- // Otherwise we deadlock the test here when we wait for the store to complete and
- // at the same time the middleware dispatches a coroutine on the dispatcher which will
- // also block on the store in SessionManager.restore().
- store.dispatch(UndoAction.RestoreRecoverableTabs).joinBlocking()
- }
-
- store.waitUntilIdle()
+ restoreRecoverableTabs(dispatcher, store)
assertNull(store.state.undoHistory.selectedTabId)
assertTrue(store.state.undoHistory.tabs.isEmpty())
@@ -296,13 +238,11 @@ class UndoMiddlewareTest {
}
@Test
- fun `Undo History gets cleared after time`() {
- val waitDispatcher = TestCoroutineDispatcher()
- val waitScope = CoroutineScope(waitDispatcher)
+ fun `Undo History gets cleared after time`() = runTestOnMain {
val store = BrowserStore(
middleware = listOf(
- UndoMiddleware(clearAfterMillis = 60000, waitScope = waitScope)
+ UndoMiddleware(clearAfterMillis = 60000, waitScope = coroutinesTestRule.scope)
),
initialState = BrowserState(
tabs = listOf(
@@ -327,8 +267,7 @@ class UndoMiddlewareTest {
assertEquals("https://www.mozilla.org", store.state.undoHistory.tabs[0].state.url)
assertEquals("https://getpocket.com", store.state.undoHistory.tabs[1].state.url)
- waitDispatcher.advanceTimeBy(70000)
- waitDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.waitUntilIdle()
assertNull(store.state.undoHistory.selectedTabId)
@@ -336,22 +275,21 @@ class UndoMiddlewareTest {
assertEquals(1, store.state.tabs.size)
assertEquals("https://reddit.com/r/firefox", store.state.tabs[0].content.url)
- testDispatcher.withDispatchingPaused {
- // We need to pause the test dispatcher here to avoid it dispatching immediately.
- // Otherwise we deadlock the test here when we wait for the store to complete and
- // at the same time the middleware dispatches a coroutine on the dispatcher which will
- // also block on the store in SessionManager.restore().
- store.dispatch(UndoAction.RestoreRecoverableTabs).joinBlocking()
- }
+ restoreRecoverableTabs(dispatcher, store)
assertEquals(1, store.state.tabs.size)
assertEquals("https://reddit.com/r/firefox", store.state.tabs[0].content.url)
}
}
-private fun TestCoroutineDispatcher.withDispatchingPaused(block: () -> Unit) {
- pauseDispatcher()
- block()
- resumeDispatcher()
- advanceUntilIdle()
+private suspend fun restoreRecoverableTabs(dispatcher: TestDispatcher, store: BrowserStore) {
+ withContext(dispatcher) {
+ // We need to pause the test dispatcher here to avoid it dispatching immediately.
+ // Otherwise we deadlock the test here when we wait for the store to complete and
+ // at the same time the middleware dispatches a coroutine on the dispatcher which will
+ // also block on the store in SessionManager.restore().
+ store.dispatch(UndoAction.RestoreRecoverableTabs).joinBlocking()
+ }
+ dispatcher.scheduler.advanceUntilIdle()
+ store.waitUntilIdle()
}
diff --git a/components/feature/sitepermissions/src/androidTest/java/mozilla/components/feature/sitepermissions/db/OnDeviceSitePermissionsStorageTest.kt b/components/feature/sitepermissions/src/androidTest/java/mozilla/components/feature/sitepermissions/db/OnDeviceSitePermissionsStorageTest.kt
index 267720b906e..42c29e24fbc 100644
--- a/components/feature/sitepermissions/src/androidTest/java/mozilla/components/feature/sitepermissions/db/OnDeviceSitePermissionsStorageTest.kt
+++ b/components/feature/sitepermissions/src/androidTest/java/mozilla/components/feature/sitepermissions/db/OnDeviceSitePermissionsStorageTest.kt
@@ -11,7 +11,7 @@ import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus
import mozilla.components.concept.engine.permission.SitePermissions.Status
@@ -56,7 +56,7 @@ class OnDeviceSitePermissionsStorageTest {
}
@Test
- fun testStorageInteraction() = runBlockingTest {
+ fun testStorageInteraction() = runTest {
val origin = "https://www.mozilla.org".toUri().host!!
val sitePermissions = SitePermissions(
origin = origin,
diff --git a/components/feature/sitepermissions/src/main/res/values-ast/strings.xml b/components/feature/sitepermissions/src/main/res/values-ast/strings.xml
index 1ef515b4ba5..f8c586ece35 100644
--- a/components/feature/sitepermissions/src/main/res/values-ast/strings.xml
+++ b/components/feature/sitepermissions/src/main/res/values-ast/strings.xml
@@ -1,24 +1,24 @@
- ¿Permitir a %1$s qu\'unvie avisos?
+ ¿Quies permitir a «%1$s» qu\'unvie avisos?
- ¿Permitir a %1$s qu\'use la cámara?
+ ¿Quies permitir a «%1$s» qu\'use la cámara?
- ¿Permitir a %1$s qu\'use\'l micrófonu?
+ ¿Quies permitir a «%1$s» qu\'use\'l micrófonu?
- ¿Permitr a %1$s qu\'use\'l to allugamientu?
+ ¿Quies permitir a «%1$s» qu\'use\'l to allugamientu?
- ¿Permitir a %1$s qu\'use la cámara y el micrófonu?
+ ¿Quies permitir a «%1$s» qu\'use la cámara y el micrófonu?Micrófonu 1
- Cámara trasera
+ Cámara d\'atrás
- Cámara delantera
+ Cámara d\'alantrePermitir
@@ -32,9 +32,15 @@
Enxamás
- ¿Permitir a %1$s qu\'atroxe datos nel almacenamientu permanente?
+ ¿Quies permitir a «%1$s» qu\'atroxe datos nel almacenamientu permanente?
- ¿Permitir a %1$s que reproduza conteníu con DRM?
+ ¿Quies permitir a «%1$s» que reproduza conteníu con DRM?
+
+ ¿Quies permitir a «%1$s» qu\'use les sos cookies en «%2$s»?
+
+ Si nun ta claro por qué %s precisa estos datos, ye probable que quieras bloquiar l\'accesu.Saber más
diff --git a/components/feature/sitepermissions/src/main/res/values-bg/strings.xml b/components/feature/sitepermissions/src/main/res/values-bg/strings.xml
index f437d82e6d4..cea9abafa55 100644
--- a/components/feature/sitepermissions/src/main/res/values-bg/strings.xml
+++ b/components/feature/sitepermissions/src/main/res/values-bg/strings.xml
@@ -35,4 +35,6 @@
Разрешение %1$s да запазва в постоянно хранилище?Разрешение %1$s да възпроизвежда контролирано с DRM съдържание?
+
+ Научете повече
diff --git a/components/feature/sitepermissions/src/main/res/values-eo/strings.xml b/components/feature/sitepermissions/src/main/res/values-eo/strings.xml
index 9aad696e6d8..28326bfb5cf 100644
--- a/components/feature/sitepermissions/src/main/res/values-eo/strings.xml
+++ b/components/feature/sitepermissions/src/main/res/values-eo/strings.xml
@@ -35,6 +35,12 @@
Ĉu permesi al %1$s konservi datumojn en konstanta konservejo?Ĉu permesi al %1$s ludi enhavon protektitan de DRM?
+
+ Ĉu permesi al %1$s uzi siajn kuketojn en %2$s?
+
+ Vi povus voli bloki la aliron, se ne estas klare, kial %s bezonas tiujn ĉi datumojn.Pli da informo
diff --git a/components/feature/sitepermissions/src/main/res/values-skr/strings.xml b/components/feature/sitepermissions/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..50e651ad969
--- /dev/null
+++ b/components/feature/sitepermissions/src/main/res/values-skr/strings.xml
@@ -0,0 +1,46 @@
+
+
+
+ %1$s کوں اطلاعاں پٹھݨ دی اجازت ݙیندے ہو؟
+
+ %1$s کوں آپݨاں کیمرہ ورتݨ دی اجازت ݙیندے ہو؟
+
+ %1$s کوں آپݨاں مائیکروفون ورتݨ دی اجازت ݙیندے ہو؟
+
+ %1$s کوں آپݨاں مقام ورتݨ دی اجازت ݙیندے ہو؟
+
+ %1$s کوں آپݨاں کیمرہ تے مائیکروفون ورتݨ دی اجازت ݙیندے ہو؟
+
+ مائیکروفون ١
+
+ پچھوں آلا کیمرہ
+
+ سامھِݨے آلا کیمرہ
+
+ اجازت ݙیوو
+
+ اجازت نہ ݙیوو
+
+ ایں سائٹ کیتے فیصلہ یاد رکھو
+
+ ہمیشہ
+
+ کݙاہیں نہ
+
+ %1$s کوں مستقل ذخیرے وچ ڈیٹا ذخیرہ کرݨ دی اجازت ݙیووں؟
+
+ %1$s کوں ڈی آر ایم دے کنٹرول تھئے مواد چلاوݨ دی اجازت ݙیووں؟
+
+ %1$s کوں آپݨیاں کوکیاں %2$s تے ورتݨ دی اجازت ݙیووں؟
+
+ جے ایہ واضح کائنی جو %s کوں ایں ڈیٹا دی لوڑ کیوں ہے تاں تساں رسائی تے پابندی لاوݨ پسند کریسو۔
+
+ ٻیا سِکھو
+
diff --git a/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorageTest.kt b/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorageTest.kt
index cad65c5e575..1bbdf6694c7 100644
--- a/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorageTest.kt
+++ b/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorageTest.kt
@@ -10,7 +10,7 @@ import androidx.room.DatabaseConfiguration
import androidx.room.InvalidationTracker
import androidx.sqlite.db.SupportSQLiteOpenHelper
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.engine.DataCleanable
import mozilla.components.concept.engine.Engine.BrowsingData
import mozilla.components.concept.engine.permission.SitePermissions
@@ -60,7 +60,7 @@ class OnDiskSitePermissionsStorageTest {
}
@Test
- fun `save a new SitePermission`() = runBlockingTest {
+ fun `save a new SitePermission`() = runTest {
val sitePermissions = createNewSitePermission()
storage.save(sitePermissions)
@@ -69,7 +69,7 @@ class OnDiskSitePermissionsStorageTest {
}
@Test
- fun `update a SitePermission`() = runBlockingTest {
+ fun `update a SitePermission`() = runTest {
val sitePermissions = createNewSitePermission()
storage.update(sitePermissions)
@@ -80,14 +80,14 @@ class OnDiskSitePermissionsStorageTest {
}
@Test
- fun `find a SitePermissions by origin`() = runBlockingTest {
+ fun `find a SitePermissions by origin`() = runTest {
storage.findSitePermissionsBy("mozilla.org")
verify(mockDAO).getSitePermissionsBy("mozilla.org")
}
@Test
- fun `find all sitePermissions grouped by permission`() = runBlockingTest {
+ fun `find all sitePermissions grouped by permission`() = runTest {
doReturn(dummySitePermissionEntitiesList())
.`when`(mockDAO).getSitePermissions()
@@ -106,7 +106,7 @@ class OnDiskSitePermissionsStorageTest {
}
@Test
- fun `remove a SitePermissions`() = runBlockingTest {
+ fun `remove a SitePermissions`() = runTest {
val sitePermissions = createNewSitePermission()
storage.remove(sitePermissions)
@@ -118,7 +118,7 @@ class OnDiskSitePermissionsStorageTest {
}
@Test
- fun `remove all SitePermissions`() = runBlockingTest {
+ fun `remove all SitePermissions`() = runTest {
storage.removeAll()
shadowOf(getMainLooper()).idle()
@@ -127,7 +127,7 @@ class OnDiskSitePermissionsStorageTest {
}
@Test
- fun `get all SitePermissions paged`() = runBlockingTest {
+ fun `get all SitePermissions paged`() = runTest {
val mockDataSource: DataSource = mock()
doReturn(object : DataSource.Factory() {
diff --git a/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsFeatureTest.kt b/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsFeatureTest.kt
index e5fe60b0aaf..3751f0876f5 100644
--- a/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsFeatureTest.kt
+++ b/components/feature/sitepermissions/src/test/java/mozilla/components/feature/sitepermissions/SitePermissionsFeatureTest.kt
@@ -9,14 +9,6 @@ import android.content.pm.PackageManager.PERMISSION_GRANTED
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.runBlockingTest
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction
import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.AutoPlayAudibleBlockingAction
@@ -63,13 +55,15 @@ import mozilla.components.support.test.eq
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import mozilla.components.support.test.whenever
-import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
@@ -98,8 +92,9 @@ class SitePermissionsFeatureTest {
private lateinit var mockSitePermissionRules: SitePermissionsRules
private lateinit var selectedTab: TabSessionState
- private val testCoroutineDispatcher = TestCoroutineDispatcher()
- private val testScope = CoroutineScope(testCoroutineDispatcher)
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val scope = coroutinesTestRule.scope
companion object {
const val SESSION_ID = "testSessionId"
@@ -109,7 +104,6 @@ class SitePermissionsFeatureTest {
@Before
fun setup() {
- Dispatchers.setMain(testCoroutineDispatcher)
mockOnNeedToRequestPermissions = mock()
mockStorage = mock()
mockFragmentManager = mockFragmentManager()
@@ -137,13 +131,6 @@ class SitePermissionsFeatureTest {
)
}
- @After
- @ExperimentalCoroutinesApi
- fun tearDown() {
- Dispatchers.resetMain()
- testCoroutineDispatcher.cleanupTestCoroutines()
- }
-
@Test
fun `GIVEN a tab load THEN stale permission indicators should be clear up and temporary permissions`() {
sitePermissionFeature.start()
@@ -379,7 +366,7 @@ class SitePermissionsFeatureTest {
}
@Test
- fun `GIVEN a new permissionRequest WHEN storeSitePermissions() THEN save(permissionRequest) is called`() = runBlockingTest {
+ fun `GIVEN a new permissionRequest WHEN storeSitePermissions() THEN save(permissionRequest) is called`() = runTestOnMain {
// given
val sitePermissions = SitePermissions(origin = "origin", savedAt = 0)
doReturn(null).`when`(mockStorage).findSitePermissionsBy(ArgumentMatchers.anyString(), anyBoolean())
@@ -399,7 +386,7 @@ class SitePermissionsFeatureTest {
mockContentState,
mockPermissionRequest,
ALLOWED,
- testScope
+ scope
)
// then
@@ -412,7 +399,7 @@ class SitePermissionsFeatureTest {
}
@Test
- fun `GIVEN an already saved permissionRequest WHEN storeSitePermissions() THEN update(permissionRequest) is called`() = runBlockingTest {
+ fun `GIVEN an already saved permissionRequest WHEN storeSitePermissions() THEN update(permissionRequest) is called`() = runTestOnMain {
// given
val sitePermissions = SitePermissions(origin = "origin", savedAt = 0)
doReturn(sitePermissions).`when`(mockStorage)
@@ -429,7 +416,7 @@ class SitePermissionsFeatureTest {
mockContentState,
mockPermissionRequest,
ALLOWED,
- testScope
+ scope
)
// then
@@ -437,13 +424,13 @@ class SitePermissionsFeatureTest {
}
@Test
- fun `GIVEN a permissionRequest WITH a private tab WHEN storeSitePermissions() THEN save or update MUST NOT BE called`() = runBlockingTest {
+ fun `GIVEN a permissionRequest WITH a private tab WHEN storeSitePermissions() THEN save or update MUST NOT BE called`() = runTestOnMain {
// then
sitePermissionFeature.storeSitePermissions(
selectedTab.content.copy(private = true),
mockPermissionRequest,
ALLOWED,
- testScope
+ scope
)
// when
@@ -512,7 +499,7 @@ class SitePermissionsFeatureTest {
doNothing().`when`(mockPermissionRequest).reject()
// when
- runBlockingTest {
+ runTestOnMain {
sitePermissionFeature.onContentPermissionRequested(mockPermissionRequest, URL)
}
@@ -522,7 +509,7 @@ class SitePermissionsFeatureTest {
}
@Test
- fun `GIVEN location permissionRequest and shouldApplyRules is true WHEN onContentPermissionRequested() THEN handleRuledFlow is called`() = runBlockingTest {
+ fun `GIVEN location permissionRequest and shouldApplyRules is true WHEN onContentPermissionRequested() THEN handleRuledFlow is called`() = runTestOnMain {
// given
val mockPermissionRequest: PermissionRequest = mock {
whenever(permissions).thenReturn(listOf(ContentGeoLocation(id = "permission")))
@@ -535,11 +522,11 @@ class SitePermissionsFeatureTest {
.handleRuledFlow(mockPermissionRequest, URL)
// when
- runBlockingTest {
+ runTestOnMain {
sitePermissionFeature.onContentPermissionRequested(
mockPermissionRequest,
URL,
- testScope
+ scope
)
}
@@ -549,7 +536,7 @@ class SitePermissionsFeatureTest {
}
@Test
- fun `GIVEN location permissionRequest and shouldApplyRules is false WHEN onContentPermissionRequested() THEN handleNoRuledFlow is called`() = runBlockingTest {
+ fun `GIVEN location permissionRequest and shouldApplyRules is false WHEN onContentPermissionRequested() THEN handleNoRuledFlow is called`() = runTestOnMain {
// given
val mockPermissionRequest: PermissionRequest = mock {
whenever(permissions).thenReturn(listOf(ContentGeoLocation(id = "permission")))
@@ -562,11 +549,11 @@ class SitePermissionsFeatureTest {
.handleNoRuledFlow(sitePermissions, mockPermissionRequest, URL)
// when
- runBlockingTest {
+ runTestOnMain {
sitePermissionFeature.onContentPermissionRequested(
mockPermissionRequest,
URL,
- testScope
+ scope
)
}
@@ -576,7 +563,7 @@ class SitePermissionsFeatureTest {
}
@Test
- fun `GIVEN autoplay permissionRequest and shouldApplyRules is false WHEN onContentPermissionRequested() THEN handleNoRuledFlow is called`() = runBlockingTest {
+ fun `GIVEN autoplay permissionRequest and shouldApplyRules is false WHEN onContentPermissionRequested() THEN handleNoRuledFlow is called`() = runTestOnMain {
// given
val mockPermissionRequest: PermissionRequest = mock {
whenever(permissions).thenReturn(listOf(ContentAutoPlayInaudible(id = "permission")))
@@ -590,11 +577,11 @@ class SitePermissionsFeatureTest {
.handleNoRuledFlow(sitePermissions, mockPermissionRequest, URL)
// when
- runBlockingTest {
+ runTestOnMain {
sitePermissionFeature.onContentPermissionRequested(
mockPermissionRequest,
URL,
- testScope
+ scope
)
}
@@ -604,7 +591,7 @@ class SitePermissionsFeatureTest {
}
@Test
- fun `GIVEN shouldShowPrompt with isForAutoplay false AND null permissionFromStorage THEN return true`() = runBlockingTest {
+ fun `GIVEN shouldShowPrompt with isForAutoplay false AND null permissionFromStorage THEN return true`() = runTestOnMain {
// given
val mockPermissionRequest: PermissionRequest = mock {
whenever(permissions).thenReturn(listOf(Permission.ContentGeoLocation(id = "permission")))
@@ -1054,7 +1041,7 @@ class SitePermissionsFeatureTest {
}
@Test
- fun `is SitePermission granted in the storage`() = runBlockingTest {
+ fun `is SitePermission granted in the storage`() = runTestOnMain {
val sitePermissionsList = listOf(
ContentGeoLocation(),
ContentNotification(),
@@ -1090,7 +1077,7 @@ class SitePermissionsFeatureTest {
}
@Test
- fun `is SitePermission blocked in the storage`() = runBlockingTest {
+ fun `is SitePermission blocked in the storage`() = runTestOnMain {
val sitePermissionsList = listOf(
ContentGeoLocation(),
ContentNotification(),
@@ -1181,7 +1168,7 @@ class SitePermissionsFeatureTest {
}
@Test
- fun `getInitialSitePermissions - WHEN sitePermissionsRules is present the function MUST use the sitePermissionsRules values to create a SitePermissions object`() = runBlockingTest {
+ fun `getInitialSitePermissions - WHEN sitePermissionsRules is present the function MUST use the sitePermissionsRules values to create a SitePermissions object`() = runTestOnMain {
val rules = SitePermissionsRules(
location = SitePermissionsRules.Action.BLOCKED,
@@ -1212,7 +1199,7 @@ class SitePermissionsFeatureTest {
}
@Test
- fun `any media request must be rejected WHEN system permissions are not granted first`() = runBlocking() {
+ fun `any media request must be rejected WHEN system permissions are not granted first`() = runTestOnMain {
val permissions = listOf(
ContentVideoCapture("", "back camera"),
ContentVideoCamera("", "front camera"),
@@ -1240,12 +1227,10 @@ class SitePermissionsFeatureTest {
mockStorage = mock()
- runBlocking {
- val prompt = sitePermissionFeature
- .onContentPermissionRequested(permissionRequest, URL)
- assertNull(prompt)
- assertFalse(grantWasCalled)
- }
+ val prompt = sitePermissionFeature
+ .onContentPermissionRequested(permissionRequest, URL)
+ assertNull(prompt)
+ assertFalse(grantWasCalled)
}
Unit
diff --git a/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/controller/DefaultController.kt b/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/controller/DefaultController.kt
index 53d601b2ec7..1abfab6923d 100644
--- a/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/controller/DefaultController.kt
+++ b/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/controller/DefaultController.kt
@@ -29,8 +29,6 @@ internal class DefaultController(
* See [SyncedTabsController.refreshSyncedTabs]
*/
override fun refreshSyncedTabs() {
- view.startLoading()
-
scope.launch {
accountManager.withConstellation {
val syncedDeviceTabs = provider.getSyncedDeviceTabs()
@@ -39,7 +37,7 @@ internal class DefaultController(
scope.launch(Dispatchers.Main) {
if (syncedDeviceTabs.isEmpty() && otherDevices?.isEmpty() == true) {
view.onError(ErrorType.MULTIPLE_DEVICES_UNAVAILABLE)
- } else if (!syncedDeviceTabs.any { it.tabs.isNotEmpty() }) {
+ } else if (syncedDeviceTabs.all { it.tabs.isEmpty() }) {
view.onError(ErrorType.NO_TABS_AVAILABLE)
} else {
view.displaySyncedTabs(syncedDeviceTabs)
@@ -57,6 +55,7 @@ internal class DefaultController(
* See [SyncedTabsController.syncAccount]
*/
override fun syncAccount() {
+ view.startLoading()
scope.launch {
accountManager.withConstellation { refreshDevices() }
accountManager.syncNow(SyncReason.User, customEngineSubset = listOf(SyncEngine.Tabs))
diff --git a/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenter.kt b/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenter.kt
index f4320eaa52d..bd6b20e602c 100644
--- a/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenter.kt
+++ b/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenter.kt
@@ -77,7 +77,6 @@ internal class DefaultPresenter(
return
}
- controller.refreshSyncedTabs()
controller.syncAccount()
}
@@ -86,8 +85,11 @@ internal class DefaultPresenter(
}
companion object {
+ // This status isn't always set before it's inspected. This causes erroneous reports of the
+ // sync engine being unavailable. Tabs are included in sync by default, so it's safe to
+ // default to true until they are deliberately disabled.
private fun isSyncedTabsEngineEnabled(context: Context): Boolean {
- return SyncEnginesStorage(context).getStatus()[SyncEngine.Tabs] ?: false
+ return SyncEnginesStorage(context).getStatus()[SyncEngine.Tabs] ?: true
}
}
@@ -103,7 +105,6 @@ internal class DefaultPresenter(
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
CoroutineScope(Dispatchers.Main).launch {
controller.syncAccount()
- controller.refreshSyncedTabs()
}
}
diff --git a/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorage.kt b/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorage.kt
index 14f2b6e9300..321b90ae491 100644
--- a/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorage.kt
+++ b/components/feature/syncedtabs/src/main/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorage.kt
@@ -38,7 +38,7 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
class SyncedTabsStorage(
private val accountManager: FxaAccountManager,
private val store: BrowserStore,
- private val tabsStorage: RemoteTabsStorage = RemoteTabsStorage(),
+ private val tabsStorage: RemoteTabsStorage,
private val debounceMillis: Long = 1000L,
) : SyncedTabsProvider {
private var scope: CoroutineScope? = null
diff --git a/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsStorageSuggestionProviderTest.kt b/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsStorageSuggestionProviderTest.kt
index f162e9a6f33..54525b62934 100644
--- a/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsStorageSuggestionProviderTest.kt
+++ b/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/SyncedTabsStorageSuggestionProviderTest.kt
@@ -6,7 +6,7 @@ package mozilla.components.feature.syncedtabs
import android.graphics.drawable.Drawable
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.browser.storage.sync.Tab
import mozilla.components.browser.storage.sync.TabEntry
@@ -38,7 +38,7 @@ class SyncedTabsStorageSuggestionProviderTest {
}
@Test
- fun `matches remote tabs`() = runBlocking {
+ fun `matches remote tabs`() = runTest {
val provider = SyncedTabsStorageSuggestionProvider(syncedTabs, mock(), mock(), indicatorIcon)
val deviceTabs1 = SyncedDeviceTabs(
Device(
diff --git a/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/controller/DefaultControllerTest.kt b/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/controller/DefaultControllerTest.kt
index 8b2ad5e12cf..c25f4ff1844 100644
--- a/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/controller/DefaultControllerTest.kt
+++ b/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/controller/DefaultControllerTest.kt
@@ -5,7 +5,6 @@
package mozilla.components.feature.syncedtabs.controller
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.concept.sync.ConstellationState
import mozilla.components.concept.sync.DeviceConstellation
@@ -18,6 +17,7 @@ import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.service.fxa.sync.SyncReason
import mozilla.components.support.test.mock
import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -36,7 +36,7 @@ class DefaultControllerTest {
val coroutinesTestRule = MainCoroutineRule()
@Test
- fun `update view only when no account available`() = runBlockingTest {
+ fun `update view only when no account available`() = runTestOnMain {
val controller = DefaultController(
storage,
accountManager,
@@ -46,14 +46,13 @@ class DefaultControllerTest {
controller.refreshSyncedTabs()
- verify(view).startLoading()
verify(view).stopLoading()
verifyNoMoreInteractions(view)
}
@Test
- fun `notify if there are no other devices synced`() = runBlockingTest {
+ fun `notify if there are no other devices synced`() = runTestOnMain {
val controller = DefaultController(
storage,
accountManager,
@@ -77,7 +76,7 @@ class DefaultControllerTest {
}
@Test
- fun `notify if there are no tabs from other devices to sync`() = runBlockingTest {
+ fun `notify if there are no tabs from other devices to sync`() = runTestOnMain {
val controller = DefaultController(
storage,
accountManager,
@@ -102,7 +101,7 @@ class DefaultControllerTest {
}
@Test
- fun `display synced tabs`() = runBlockingTest {
+ fun `display synced tabs`() = runTestOnMain {
val controller = DefaultController(
storage,
accountManager,
@@ -130,7 +129,7 @@ class DefaultControllerTest {
}
@Test
- fun `syncAccount refreshes devices and syncs`() = runBlockingTest {
+ fun `WHEN syncAccount is called THEN view is loading, devices are refreshed, and sync started`() = runTestOnMain {
val controller = DefaultController(
storage,
accountManager,
@@ -146,6 +145,7 @@ class DefaultControllerTest {
controller.syncAccount()
+ verify(view).startLoading()
verify(constellation).refreshDevices()
verify(accountManager).syncNow(SyncReason.User, false, listOf(SyncEngine.Tabs))
}
diff --git a/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/interactor/DefaultInteractorTest.kt b/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/interactor/DefaultInteractorTest.kt
index a2ca4201249..ebe17b42a97 100644
--- a/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/interactor/DefaultInteractorTest.kt
+++ b/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/interactor/DefaultInteractorTest.kt
@@ -4,7 +4,7 @@
package mozilla.components.feature.syncedtabs.interactor
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.controller.SyncedTabsController
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
@@ -21,7 +21,7 @@ class DefaultInteractorTest {
private val controller: SyncedTabsController = mock()
@Test
- fun start() = runBlockingTest {
+ fun start() = runTest {
val view =
TestSyncedTabsView()
val feature = DefaultInteractor(
@@ -37,7 +37,7 @@ class DefaultInteractorTest {
}
@Test
- fun stop() = runBlockingTest {
+ fun stop() = runTest {
val view =
TestSyncedTabsView()
val feature = DefaultInteractor(
@@ -57,7 +57,7 @@ class DefaultInteractorTest {
}
@Test
- fun `onTabClicked invokes callback`() = runBlockingTest {
+ fun `onTabClicked invokes callback`() = runTest {
var invoked = false
val feature = DefaultInteractor(
controller,
@@ -72,7 +72,7 @@ class DefaultInteractorTest {
}
@Test
- fun `onRefresh does not update devices when there is no constellation`() = runBlockingTest {
+ fun `onRefresh does not update devices when there is no constellation`() = runTest {
val feature = DefaultInteractor(
controller,
view
@@ -84,7 +84,7 @@ class DefaultInteractorTest {
}
@Test
- fun `onRefresh updates devices when there is a constellation`() = runBlockingTest {
+ fun `onRefresh updates devices when there is a constellation`() = runTest {
val feature = DefaultInteractor(
controller,
view
diff --git a/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenterTest.kt b/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenterTest.kt
index 9f8d4733b9e..b43a89555f8 100644
--- a/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenterTest.kt
+++ b/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/presenter/DefaultPresenterTest.kt
@@ -8,7 +8,7 @@ import android.content.Context
import android.os.Looper.getMainLooper
import androidx.lifecycle.LifecycleOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.feature.syncedtabs.controller.SyncedTabsController
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
@@ -35,7 +35,7 @@ class DefaultPresenterTest {
private val prefs = testContext.getSharedPreferences(SYNC_ENGINES_KEY, Context.MODE_PRIVATE)
@Test
- fun `start returns when there is no profile`() = runBlockingTest {
+ fun `start returns when there is no profile`() = runTest {
val presenter = DefaultPresenter(
context,
controller,
@@ -50,7 +50,7 @@ class DefaultPresenterTest {
}
@Test
- fun `start returns if sync engine is not enabled`() = runBlockingTest {
+ fun `start returns if sync engine is not enabled`() = runTest {
val presenter = DefaultPresenter(
context,
controller,
@@ -87,7 +87,7 @@ class DefaultPresenterTest {
}
@Test
- fun `start invokes syncTabs - account profile is absent`() = runBlockingTest {
+ fun `start invokes syncTabs - account profile is absent`() = runTest {
val presenter = DefaultPresenter(
context,
controller,
@@ -107,7 +107,7 @@ class DefaultPresenterTest {
}
@Test
- fun `start invokes syncTabs - account profile is present`() = runBlockingTest {
+ fun `start invokes syncTabs - account profile is present`() = runTest {
val presenter = DefaultPresenter(
context,
controller,
@@ -127,7 +127,7 @@ class DefaultPresenterTest {
}
@Test
- fun `notify on logout`() = runBlockingTest {
+ fun `notify on logout`() = runTest {
val presenter = DefaultPresenter(
context,
controller,
@@ -143,7 +143,7 @@ class DefaultPresenterTest {
}
@Test
- fun `notify on authenticated`() = runBlockingTest {
+ fun `notify on authenticated`() = runTest {
val presenter = DefaultPresenter(
context,
controller,
@@ -156,11 +156,10 @@ class DefaultPresenterTest {
shadowOf(getMainLooper()).idle()
verify(controller).syncAccount()
- verify(controller).refreshSyncedTabs()
}
@Test
- fun `notify on authentication problems`() = runBlockingTest {
+ fun `notify on authentication problems`() = runTest {
val presenter = DefaultPresenter(
context,
controller,
@@ -176,7 +175,7 @@ class DefaultPresenterTest {
}
@Test
- fun `sync tabs on idle status - tabs sync enabled`() = runBlockingTest {
+ fun `sync tabs on idle status - tabs sync enabled`() = runTest {
val presenter = DefaultPresenter(
context,
controller,
@@ -192,7 +191,7 @@ class DefaultPresenterTest {
}
@Test
- fun `sync tabs on idle status - tabs sync disabled`() = runBlockingTest {
+ fun `sync tabs on idle status - tabs sync disabled`() = runTest {
val presenter = DefaultPresenter(
context,
controller,
@@ -209,7 +208,7 @@ class DefaultPresenterTest {
}
@Test
- fun `show loading state on started status`() = runBlockingTest {
+ fun `show loading state on started status`() = runTest {
val presenter = DefaultPresenter(
context,
controller,
@@ -224,7 +223,7 @@ class DefaultPresenterTest {
}
@Test
- fun `notify on error`() = runBlockingTest {
+ fun `notify on error`() = runTest {
val presenter = DefaultPresenter(
context,
controller,
diff --git a/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorageTest.kt b/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorageTest.kt
index d5d4e51a283..f7316fcc60e 100644
--- a/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorageTest.kt
+++ b/components/feature/syncedtabs/src/test/java/mozilla/components/feature/syncedtabs/storage/SyncedTabsStorageTest.kt
@@ -10,10 +10,6 @@
package mozilla.components.feature.syncedtabs.storage
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.runBlockingTest
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.LastAccessAction
import mozilla.components.browser.state.action.TabListAction
@@ -36,9 +32,12 @@ import mozilla.components.service.fxa.sync.SyncReason
import mozilla.components.support.test.any
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import mozilla.components.support.test.whenever
import org.junit.Assert.assertEquals
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.never
@@ -48,6 +47,9 @@ import org.mockito.Mockito.times
import org.mockito.Mockito.verify
class SyncedTabsStorageTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
private lateinit var store: BrowserStore
private lateinit var tabsStorage: RemoteTabsStorage
private lateinit var accountManager: FxaAccountManager
@@ -68,12 +70,10 @@ class SyncedTabsStorageTest {
)
tabsStorage = mock()
accountManager = mock()
-
- Dispatchers.setMain(TestCoroutineDispatcher())
}
@Test
- fun `listens to browser store changes, stores state changes, and calls onStoreComplete`() = runBlockingTest {
+ fun `listens to browser store changes, stores state changes, and calls onStoreComplete`() = runTestOnMain {
val feature = SyncedTabsStorage(
accountManager,
store,
@@ -98,7 +98,7 @@ class SyncedTabsStorageTest {
}
@Test
- fun `stops listening to browser store changes on stop()`() = runBlockingTest {
+ fun `stops listening to browser store changes on stop()`() = runTestOnMain {
val feature = SyncedTabsStorage(
accountManager,
store,
@@ -124,7 +124,7 @@ class SyncedTabsStorageTest {
}
@Test
- fun `getSyncedTabs matches tabs with FxA devices`() = runBlockingTest {
+ fun `getSyncedTabs matches tabs with FxA devices`() = runTestOnMain {
val feature = spy(
SyncedTabsStorage(
accountManager,
@@ -171,7 +171,7 @@ class SyncedTabsStorageTest {
}
@Test
- fun `getSyncedTabs returns empty list if syncClients() is null`() = runBlockingTest {
+ fun `getSyncedTabs returns empty list if syncClients() is null`() = runTestOnMain {
val feature = spy(
SyncedTabsStorage(
accountManager,
@@ -245,7 +245,7 @@ class SyncedTabsStorageTest {
}
@Test
- fun `tabs are stored when loaded`() = runBlockingTest {
+ fun `tabs are stored when loaded`() = runTestOnMain {
val store = BrowserStore(
BrowserState(
tabs = listOf(
@@ -282,7 +282,7 @@ class SyncedTabsStorageTest {
}
@Test
- fun `only loaded tabs are stored on load`() = runBlockingTest {
+ fun `only loaded tabs are stored on load`() = runTestOnMain {
val store = BrowserStore(
BrowserState(
tabs = listOf(
@@ -312,7 +312,7 @@ class SyncedTabsStorageTest {
}
@Test
- fun `tabs are stored when selected tab changes`() = runBlockingTest {
+ fun `tabs are stored when selected tab changes`() = runTestOnMain {
val store = BrowserStore(
BrowserState(
tabs = listOf(
@@ -343,7 +343,7 @@ class SyncedTabsStorageTest {
}
@Test
- fun `tabs are stored when lastAccessed is changed for any tab`() = runBlockingTest {
+ fun `tabs are stored when lastAccessed is changed for any tab`() = runTestOnMain {
val store = BrowserStore(
BrowserState(
tabs = listOf(
diff --git a/components/feature/tab-collections/build.gradle b/components/feature/tab-collections/build.gradle
index c0ef3cbd9f7..48c1d43fcd6 100644
--- a/components/feature/tab-collections/build.gradle
+++ b/components/feature/tab-collections/build.gradle
@@ -81,6 +81,7 @@ dependencies {
androidTestImplementation Dependencies.androidx_test_core
androidTestImplementation Dependencies.androidx_test_runner
androidTestImplementation Dependencies.androidx_test_rules
+ androidTestImplementation Dependencies.testing_coroutines
}
apply from: '../../../publish.gradle'
diff --git a/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/TabCollectionStorageTest.kt b/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/TabCollectionStorageTest.kt
index 98b8a76dcd0..e13588b99d7 100644
--- a/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/TabCollectionStorageTest.kt
+++ b/components/feature/tab-collections/src/androidTest/java/mozilla/components/feature/tab/collections/TabCollectionStorageTest.kt
@@ -9,8 +9,9 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.paging.PagedList
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.state.recover.RecoverableTab
@@ -27,6 +28,7 @@ import org.junit.Test
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
+@ExperimentalCoroutinesApi // for runTest
@Suppress("LargeClass") // Large test is large
class TabCollectionStorageTest {
private lateinit var context: Context
@@ -220,7 +222,7 @@ class TabCollectionStorageTest {
@Test
@Suppress("ComplexMethod")
- fun testGettingCollections() = runBlocking {
+ fun testGettingCollections() = runTest {
storage.createCollection(
"Articles",
listOf(
@@ -302,7 +304,7 @@ class TabCollectionStorageTest {
@Test
@Suppress("ComplexMethod")
- fun testGettingCollectionsList() = runBlocking {
+ fun testGettingCollectionsList() = runTest {
storage.createCollection(
"Articles",
listOf(
@@ -382,7 +384,7 @@ class TabCollectionStorageTest {
}
@Test
- fun testGettingTabCollectionCount() = runBlocking {
+ fun testGettingTabCollectionCount() = runTest {
assertEquals(0, storage.getTabCollectionsCount())
storage.createCollection(
diff --git a/components/feature/tabs/src/main/res/values-skr/strings.xml b/components/feature/tabs/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..52923cf7506
--- /dev/null
+++ b/components/feature/tabs/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+
+
+
+ ٹیباں
+
diff --git a/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt b/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt
index 9f26f507eeb..d54600ad1d0 100644
--- a/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt
+++ b/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt
@@ -4,7 +4,7 @@
package mozilla.components.feature.tabs
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.TabListAction
@@ -165,11 +165,11 @@ class TabsUseCasesTest {
// Wait for CreateEngineSessionAction and middleware
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
// Wait for LinkEngineSessionAction and middleware
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertEquals(1, store.state.tabs.size)
assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
@@ -180,11 +180,13 @@ class TabsUseCasesTest {
fun `AddNewTabUseCase forwards load flags to engine`() {
tabsUseCases.addTab.invoke("https://www.mozilla.org", flags = LoadUrlFlags.external(), startLoading = true)
+ // Wait for CreateEngineSessionAction and middleware
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+ // Wait for LinkEngineSessionAction and middleware
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertEquals(1, store.state.tabs.size)
assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
@@ -265,11 +267,11 @@ class TabsUseCasesTest {
// Wait for CreateEngineSessionAction and middleware
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
// Wait for LinkEngineSessionAction and middleware
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertEquals(1, store.state.tabs.size)
assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
@@ -281,11 +283,13 @@ class TabsUseCasesTest {
fun `AddNewPrivateTabUseCase forwards load flags to engine`() {
tabsUseCases.addPrivateTab.invoke("https://www.mozilla.org", flags = LoadUrlFlags.external(), startLoading = true)
+ // Wait for CreateEngineSessionAction and middleware
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
+ // Wait for LinkEngineSessionAction and middleware
store.waitUntilIdle()
- dispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertEquals(1, store.state.tabs.size)
assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
@@ -345,7 +349,7 @@ class TabsUseCasesTest {
}
@Test
- fun `RestoreUseCase - filters based on tab timeout`() = runBlocking {
+ fun `RestoreUseCase - filters based on tab timeout`() = runTest {
val useCases = TabsUseCases(BrowserStore())
val now = System.currentTimeMillis()
diff --git a/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/WindowFeatureTest.kt b/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/WindowFeatureTest.kt
index 19fe59b55a1..3fb33400b62 100644
--- a/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/WindowFeatureTest.kt
+++ b/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/WindowFeatureTest.kt
@@ -29,7 +29,6 @@ class WindowFeatureTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val testDispatcher = coroutinesTestRule.testDispatcher
private lateinit var store: BrowserStore
private lateinit var engineSession: EngineSession
@@ -70,7 +69,7 @@ class WindowFeatureTest {
whenever(windowRequest.url).thenReturn("https://www.firefox.com")
store.dispatch(ContentAction.UpdateWindowRequestAction(tabId, windowRequest)).joinBlocking()
- testDispatcher.advanceUntilIdle()
+
verify(addTabUseCase).invoke(url = "about:blank", selectTab = true, parentId = tabId)
verify(store).dispatch(ContentAction.ConsumeWindowRequestAction(tabId))
}
@@ -86,7 +85,7 @@ class WindowFeatureTest {
store.dispatch(TabListAction.SelectTabAction(privateTabId)).joinBlocking()
store.dispatch(ContentAction.UpdateWindowRequestAction(privateTabId, windowRequest)).joinBlocking()
- testDispatcher.advanceUntilIdle()
+
verify(addTabUseCase).invoke(url = "about:blank", selectTab = true, parentId = privateTabId, private = true)
verify(store).dispatch(ContentAction.ConsumeWindowRequestAction(privateTabId))
}
@@ -101,7 +100,7 @@ class WindowFeatureTest {
whenever(windowRequest.prepare()).thenReturn(engineSession)
store.dispatch(ContentAction.UpdateWindowRequestAction(tabId, windowRequest)).joinBlocking()
- testDispatcher.advanceUntilIdle()
+
verify(removeTabUseCase).invoke(tabId)
verify(store).dispatch(ContentAction.ConsumeWindowRequestAction(tabId))
}
@@ -116,7 +115,7 @@ class WindowFeatureTest {
whenever(windowRequest.type).thenReturn(WindowRequest.Type.CLOSE)
store.dispatch(ContentAction.UpdateWindowRequestAction(tabId, windowRequest)).joinBlocking()
- testDispatcher.advanceUntilIdle()
+
verify(removeTabUseCase, never()).invoke(tabId)
verify(store, never()).dispatch(ContentAction.ConsumeWindowRequestAction(tabId))
}
diff --git a/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt b/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt
index 89854ce1a0d..1f4633a19f7 100644
--- a/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt
+++ b/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt
@@ -5,11 +5,6 @@
package mozilla.components.feature.tabs.tabstray
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabPartition
@@ -18,13 +13,13 @@ import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.support.test.ext.joinBlocking
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
-import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.spy
@@ -32,19 +27,9 @@ import org.mockito.Mockito.verifyNoMoreInteractions
@RunWith(AndroidJUnit4::class)
class TabsTrayPresenterTest {
- private val testDispatcher = TestCoroutineDispatcher()
-
- @Before
- fun setUp() {
- Dispatchers.setMain(testDispatcher)
- }
-
- @After
- @ExperimentalCoroutinesApi
- fun tearDown() {
- Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
- }
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
@Test
fun `initial set of sessions will be passed to tabs tray`() {
@@ -71,7 +56,7 @@ class TabsTrayPresenterTest {
presenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertNotNull(tabsTray.updateTabs)
@@ -106,7 +91,7 @@ class TabsTrayPresenterTest {
presenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertEquals(2, tabsTray.updateTabs!!.size)
@@ -144,17 +129,17 @@ class TabsTrayPresenterTest {
presenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertEquals(2, tabsTray.updateTabs!!.size)
store.dispatch(TabListAction.RemoveTabAction("a")).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertEquals(1, tabsTray.updateTabs!!.size)
store.dispatch(TabListAction.RemoveTabAction("b")).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertEquals(0, tabsTray.updateTabs!!.size)
@@ -184,12 +169,12 @@ class TabsTrayPresenterTest {
presenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertEquals(2, tabsTray.updateTabs!!.size)
store.dispatch(TabListAction.RemoveAllTabsAction()).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertEquals(0, tabsTray.updateTabs!!.size)
@@ -221,13 +206,13 @@ class TabsTrayPresenterTest {
)
presenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertEquals(5, tabsTray.updateTabs!!.size)
assertEquals("a", tabsTray.selectedTabId)
store.dispatch(TabListAction.SelectTabAction("d")).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
println("Selection: " + store.state.selectedTabId)
assertEquals("d", tabsTray.selectedTabId)
@@ -255,7 +240,7 @@ class TabsTrayPresenterTest {
)
presenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertTrue(tabsTray.updateTabs?.size == 1)
}
@@ -287,12 +272,12 @@ class TabsTrayPresenterTest {
)
presenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
Assert.assertFalse(closed)
store.dispatch(TabListAction.RemoveAllTabsAction()).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertTrue(closed)
@@ -323,17 +308,17 @@ class TabsTrayPresenterTest {
)
presenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
Assert.assertFalse(closed)
store.dispatch(TabListAction.RemoveTabAction("a")).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
Assert.assertFalse(closed)
store.dispatch(TabListAction.RemoveTabAction("b")).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertTrue(closed)
@@ -360,7 +345,7 @@ class TabsTrayPresenterTest {
)
presenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
assertFalse(invoked)
}
diff --git a/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeature.kt b/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeature.kt
index cb9418a52bc..c2dbdc587e0 100644
--- a/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeature.kt
+++ b/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeature.kt
@@ -18,28 +18,35 @@ import mozilla.components.concept.toolbar.Toolbar
* @property toolbar the [Toolbar] to connect to autocomplete providers.
* @property engine (optional) instance of a browser [Engine] to issue
* [Engine.speculativeConnect] calls on successful URL autocompletion.
+ * @property shouldAutocomplete (optional) lambda expression that returns true if
+ * autocomplete is shown. Otherwise, autocomplete is not shown.
*/
class ToolbarAutocompleteFeature(
val toolbar: Toolbar,
- val engine: Engine? = null
+ val engine: Engine? = null,
+ val shouldAutocomplete: () -> Boolean = { true }
) {
private val historyProviders: MutableList = mutableListOf()
private val domainProviders: MutableList = mutableListOf()
init {
toolbar.setAutocompleteListener { query, delegate ->
- val historyResults = historyProviders.asSequence()
- .mapNotNull { provider -> provider.getAutocompleteSuggestion(query)?.into() }
- val domainResults = domainProviders.asSequence()
- .mapNotNull { provider -> provider.getAutocompleteSuggestion(query)?.into() }
-
- val result = (historyResults + domainResults).firstOrNull()
- if (result != null) {
- delegate.applyAutocompleteResult(result) {
- engine?.speculativeConnect(result.url)
- }
- } else {
+ if (!shouldAutocomplete()) {
delegate.noAutocompleteResult(query)
+ } else {
+ val historyResults = historyProviders.asSequence()
+ .mapNotNull { provider -> provider.getAutocompleteSuggestion(query)?.into() }
+ val domainResults = domainProviders.asSequence()
+ .mapNotNull { provider -> provider.getAutocompleteSuggestion(query)?.into() }
+
+ val result = (historyResults + domainResults).firstOrNull()
+ if (result != null) {
+ delegate.applyAutocompleteResult(result) {
+ engine?.speculativeConnect(result.url)
+ }
+ } else {
+ delegate.noAutocompleteResult(query)
+ }
}
}
}
diff --git a/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarBehaviorController.kt b/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarBehaviorController.kt
index d0645522015..4b6e89f7744 100644
--- a/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarBehaviorController.kt
+++ b/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarBehaviorController.kt
@@ -7,7 +7,6 @@ package mozilla.components.feature.toolbar
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.mapNotNull
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
@@ -47,6 +46,7 @@ class ToolbarBehaviorController(
}
if (state.content.loading) {
+ expandToolbar()
disableScrolling()
} else if (!state.content.loading) {
enableScrolling()
diff --git a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ContainerToolbarFeatureTest.kt b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ContainerToolbarFeatureTest.kt
index f73b0a55b44..7b798fc488e 100644
--- a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ContainerToolbarFeatureTest.kt
+++ b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ContainerToolbarFeatureTest.kt
@@ -33,7 +33,6 @@ class ContainerToolbarFeatureTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val testDispatcher = coroutinesTestRule.testDispatcher
@Test
fun `render a container action from browser state`() {
@@ -80,7 +79,7 @@ class ContainerToolbarFeatureTest {
)
val containerToolbarFeature = getContainerToolbarFeature(toolbar, store)
store.dispatch(TabListAction.SelectTabAction("tab2")).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ coroutinesTestRule.testDispatcher.scheduler.advanceUntilIdle()
verify(store).observeManually(any())
verify(containerToolbarFeature, times(2)).renderContainerAction(any(), any())
diff --git a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeatureTest.kt b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeatureTest.kt
index a01c429566e..0ed10c77389 100644
--- a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeatureTest.kt
+++ b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeatureTest.kt
@@ -5,7 +5,7 @@
package mozilla.components.feature.toolbar
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.domains.Domain
import mozilla.components.browser.domains.autocomplete.BaseDomainAutocompleteProvider
import mozilla.components.browser.domains.autocomplete.DomainList
@@ -112,6 +112,10 @@ class ToolbarAutocompleteFeatureTest {
fail()
}
+ override fun removeEditActionEnd(action: Toolbar.Action) {
+ fail()
+ }
+
override fun invalidateActions() {
fail()
}
@@ -138,7 +142,7 @@ class ToolbarAutocompleteFeatureTest {
}
@Test
- fun `feature can be used without providers`() {
+ fun `feature can be used without providers`() = runTest {
val toolbar = TestToolbar()
ToolbarAutocompleteFeature(toolbar)
@@ -146,9 +150,8 @@ class ToolbarAutocompleteFeatureTest {
assertNotNull(toolbar.autocompleteFilter)
val autocompleteDelegate: AutocompleteDelegate = mock()
- runBlocking {
- toolbar.autocompleteFilter!!("moz", autocompleteDelegate)
- }
+ toolbar.autocompleteFilter!!("moz", autocompleteDelegate)
+
verify(autocompleteDelegate, never()).applyAutocompleteResult(any(), any())
verify(autocompleteDelegate, times(1)).noAutocompleteResult("moz")
}
@@ -278,7 +281,7 @@ class ToolbarAutocompleteFeatureTest {
}
@Test
- fun `feature triggers speculative connect for results if engine provided`() {
+ fun `feature triggers speculative connect for results if engine provided`() = runTest {
val toolbar = TestToolbar()
val engine: Engine = mock()
var feature = ToolbarAutocompleteFeature(toolbar, engine)
@@ -292,9 +295,38 @@ class ToolbarAutocompleteFeatureTest {
domains.testDomains(listOf(Domain.create("https://www.mozilla.org")))
feature.addDomainProvider(domains)
- runBlocking {
- toolbar.autocompleteFilter!!.invoke("mo", autocompleteDelegate)
+ toolbar.autocompleteFilter!!.invoke("mo", autocompleteDelegate)
+
+ val callbackCaptor = argumentCaptor<() -> Unit>()
+ verify(autocompleteDelegate, times(1)).applyAutocompleteResult(any(), callbackCaptor.capture())
+ verify(engine, never()).speculativeConnect("https://www.mozilla.org")
+ callbackCaptor.value.invoke()
+ verify(engine).speculativeConnect("https://www.mozilla.org")
+ }
+
+ @Test
+ fun `WHEN should autocomplete returns false THEN return no results`() = runTest {
+ val toolbar = TestToolbar()
+ val engine: Engine = mock()
+ var shouldAutoComplete = false
+ val feature = ToolbarAutocompleteFeature(toolbar, engine) { shouldAutoComplete }
+ val autocompleteDelegate: AutocompleteDelegate = mock()
+
+ val domains = object : BaseDomainAutocompleteProvider(DomainList.CUSTOM, { emptyList() }) {
+ fun testDomains(list: List) {
+ domains = list
+ }
}
+ domains.testDomains(listOf(Domain.create("https://www.mozilla.org")))
+ feature.addDomainProvider(domains)
+
+ toolbar.autocompleteFilter!!.invoke("mo", autocompleteDelegate)
+
+ verify(autocompleteDelegate, times(1)).noAutocompleteResult(any())
+ verify(engine, never()).speculativeConnect("https://www.mozilla.org")
+
+ shouldAutoComplete = true
+ toolbar.autocompleteFilter!!.invoke("mo", autocompleteDelegate)
val callbackCaptor = argumentCaptor<() -> Unit>()
verify(autocompleteDelegate, times(1)).applyAutocompleteResult(any(), callbackCaptor.capture())
@@ -304,19 +336,17 @@ class ToolbarAutocompleteFeatureTest {
}
@Suppress("SameParameterValue")
- private fun verifyNoAutocompleteResult(toolbar: TestToolbar, autocompleteDelegate: AutocompleteDelegate, query: String) {
- runBlocking {
- toolbar.autocompleteFilter!!(query, autocompleteDelegate)
- }
+ private fun verifyNoAutocompleteResult(toolbar: TestToolbar, autocompleteDelegate: AutocompleteDelegate, query: String) = runTest {
+ toolbar.autocompleteFilter!!(query, autocompleteDelegate)
+
verify(autocompleteDelegate, never()).applyAutocompleteResult(any(), any())
verify(autocompleteDelegate, times(1)).noAutocompleteResult(query)
reset(autocompleteDelegate)
}
- private fun verifyAutocompleteResult(toolbar: TestToolbar, autocompleteDelegate: AutocompleteDelegate, query: String, result: AutocompleteResult) {
- runBlocking {
- toolbar.autocompleteFilter!!.invoke(query, autocompleteDelegate)
- }
+ private fun verifyAutocompleteResult(toolbar: TestToolbar, autocompleteDelegate: AutocompleteDelegate, query: String, result: AutocompleteResult) = runTest {
+ toolbar.autocompleteFilter!!.invoke(query, autocompleteDelegate)
+
verify(autocompleteDelegate, times(1)).applyAutocompleteResult(eq(result), any())
verify(autocompleteDelegate, never()).noAutocompleteResult(query)
reset(autocompleteDelegate)
diff --git a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarBehaviorControllerTest.kt b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarBehaviorControllerTest.kt
index d6bdaafb730..1ee1133fe96 100644
--- a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarBehaviorControllerTest.kt
+++ b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarBehaviorControllerTest.kt
@@ -180,4 +180,21 @@ class ToolbarBehaviorControllerTest {
verify(controller, never()).expandToolbar()
}
+
+ @Test
+ fun `GIVEN the current tab is loading an url WHEN the page is scrolled THEN expand toolbar`() {
+ val tabContent = ContentState("loading", loading = true)
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(TabSessionState("tab_1", tabContent)),
+ selectedTabId = "tab_1"
+ )
+ )
+ val controller = spy(ToolbarBehaviorController(mock(), store))
+
+ controller.start()
+ shadowOf(getMainLooper()).idle()
+
+ verify(controller).expandToolbar()
+ }
}
diff --git a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarInteractorTest.kt b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarInteractorTest.kt
index d7de85916af..bc60e356acf 100644
--- a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarInteractorTest.kt
+++ b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarInteractorTest.kt
@@ -97,6 +97,10 @@ class ToolbarInteractorTest {
fail()
}
+ override fun removeEditActionEnd(action: Toolbar.Action) {
+ fail()
+ }
+
override fun invalidateActions() {
fail()
}
diff --git a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarPresenterTest.kt b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarPresenterTest.kt
index fa521321a86..86dc2e02de5 100644
--- a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarPresenterTest.kt
+++ b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarPresenterTest.kt
@@ -4,11 +4,6 @@
package mozilla.components.feature.toolbar
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction
import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction.NotificationChangedAction
@@ -28,8 +23,8 @@ import mozilla.components.feature.toolbar.internal.URLRenderer
import mozilla.components.support.test.any
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
-import org.junit.After
-import org.junit.Before
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
@@ -38,20 +33,9 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
class ToolbarPresenterTest {
- private val testDispatcher = TestCoroutineDispatcher()
-
- @Before
- @ExperimentalCoroutinesApi
- fun setUp() {
- Dispatchers.setMain(testDispatcher)
- }
-
- @After
- @ExperimentalCoroutinesApi
- fun tearDown() {
- Dispatchers.resetMain()
- testDispatcher.cleanupTestCoroutines()
- }
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+ private val dispatcher = coroutinesTestRule.testDispatcher
@Test
fun `start with no custom tab id registers on store and renders selected tab`() {
@@ -71,7 +55,7 @@ class ToolbarPresenterTest {
toolbarPresenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(store).observeManually(any())
@@ -100,7 +84,7 @@ class ToolbarPresenterTest {
toolbarPresenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(store).observeManually(any())
verify(toolbarPresenter).render(any())
@@ -129,7 +113,7 @@ class ToolbarPresenterTest {
toolbarPresenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbar, never()).siteSecure = Toolbar.SiteSecurity.SECURE
@@ -144,7 +128,7 @@ class ToolbarPresenterTest {
)
).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbar).siteSecure = Toolbar.SiteSecurity.SECURE
}
@@ -176,7 +160,7 @@ class ToolbarPresenterTest {
toolbarPresenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbarPresenter.renderer).start()
verify(toolbarPresenter.renderer).post("https://www.mozilla.org")
@@ -190,7 +174,7 @@ class ToolbarPresenterTest {
store.dispatch(TabListAction.RemoveTabAction("tab1")).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbarPresenter.renderer).post("")
verify(toolbar).setSearchTerms("")
@@ -216,7 +200,7 @@ class ToolbarPresenterTest {
toolbarPresenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbar, never()).setSearchTerms("Hello World")
@@ -227,7 +211,7 @@ class ToolbarPresenterTest {
)
).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbar).setSearchTerms("Hello World")
}
@@ -250,7 +234,7 @@ class ToolbarPresenterTest {
toolbarPresenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbar, never()).displayProgress(75)
@@ -258,7 +242,7 @@ class ToolbarPresenterTest {
ContentAction.UpdateProgressAction("tab1", 75)
).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbar).displayProgress(75)
@@ -268,7 +252,7 @@ class ToolbarPresenterTest {
ContentAction.UpdateProgressAction("tab1", 90)
).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbar).displayProgress(90)
}
@@ -301,7 +285,7 @@ class ToolbarPresenterTest {
toolbarPresenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
store.dispatch(TabListAction.RemoveTabAction("tab2")).joinBlocking()
@@ -354,7 +338,7 @@ class ToolbarPresenterTest {
toolbarPresenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbarPresenter.renderer).start()
verify(toolbarPresenter.renderer).post("https://www.mozilla.org")
@@ -368,7 +352,7 @@ class ToolbarPresenterTest {
store.dispatch(TabListAction.SelectTabAction("tab2")).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbarPresenter.renderer).post("https://www.example.org")
verify(toolbar).setSearchTerms("Example")
@@ -407,28 +391,28 @@ class ToolbarPresenterTest {
toolbarPresenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.OFF_GLOBALLY
store.dispatch(TrackingProtectionAction.ToggleAction("tab", true))
.joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.ON_NO_TRACKERS_BLOCKED
store.dispatch(TrackingProtectionAction.TrackerBlockedAction("tab", mock()))
.joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.ON_TRACKERS_BLOCKED
store.dispatch(TrackingProtectionAction.ToggleExclusionListAction("tab", true))
.joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.OFF_FOR_A_SITE
}
@@ -460,26 +444,26 @@ class ToolbarPresenterTest {
toolbarPresenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbar).highlight = Toolbar.Highlight.NONE
store.dispatch(NotificationChangedAction("tab", true)).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbar).highlight = Toolbar.Highlight.PERMISSIONS_CHANGED
store.dispatch(TrackingProtectionAction.ToggleExclusionListAction("tab", true))
.joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbar, times(2)).highlight = Toolbar.Highlight.PERMISSIONS_CHANGED
store.dispatch(UpdatePermissionHighlightsStateAction.Reset("tab")).joinBlocking()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(toolbar).highlight = Toolbar.Highlight.NONE
}
@@ -510,7 +494,7 @@ class ToolbarPresenterTest {
presenter.start()
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(presenter.renderer).post("")
verify(toolbar).setSearchTerms("")
diff --git a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarFeatureTest.kt b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarFeatureTest.kt
index 61fb0d2cad7..f463dd07d37 100644
--- a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarFeatureTest.kt
+++ b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarFeatureTest.kt
@@ -41,7 +41,7 @@ class WebExtensionToolbarFeatureTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val testDispatcher = coroutinesTestRule.testDispatcher
+ private val dispatcher = coroutinesTestRule.testDispatcher
@Test
fun `render web extension actions from browser state`() {
@@ -75,7 +75,7 @@ class WebExtensionToolbarFeatureTest {
)
)
val webExtToolbarFeature = getWebExtensionToolbarFeature(toolbar, store)
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(store).observeManually(any())
verify(webExtToolbarFeature).renderWebExtensionActions(any(), any())
@@ -125,7 +125,7 @@ class WebExtensionToolbarFeatureTest {
)
)
val webExtToolbarFeature = getWebExtensionToolbarFeature(toolbar, store)
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(store).observeManually(any())
verify(webExtToolbarFeature, times(1)).renderWebExtensionActions(any(), any())
@@ -403,7 +403,7 @@ class WebExtensionToolbarFeatureTest {
)
)
val webExtToolbarFeature = getWebExtensionToolbarFeature(toolbar, store)
- testDispatcher.advanceUntilIdle()
+ dispatcher.scheduler.advanceUntilIdle()
verify(store).observeManually(any())
verify(webExtToolbarFeature).renderWebExtensionActions(any(), any())
diff --git a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarTest.kt b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarTest.kt
index 93b4bab19ea..6ae3267418f 100644
--- a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarTest.kt
+++ b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/WebExtensionToolbarTest.kt
@@ -61,7 +61,7 @@ class WebExtensionToolbarTest {
val action = WebExtensionToolbarAction(browserAction, iconJobDispatcher = testDispatcher) {}
action.bind(view)
action.iconJob?.joinBlocking()
- testDispatcher.advanceUntilIdle()
+ testDispatcher.scheduler.advanceUntilIdle()
val iconCaptor = argumentCaptor()
verify(imageView).setImageDrawable(iconCaptor.capture())
@@ -95,7 +95,7 @@ class WebExtensionToolbarTest {
val action = WebExtensionToolbarAction(browserAction, iconJobDispatcher = testDispatcher) {}
action.bind(view)
action.iconJob?.joinBlocking()
- testDispatcher.advanceUntilIdle()
+ testDispatcher.scheduler.advanceUntilIdle()
verify(imageView).setImageResource(R.drawable.mozac_ic_web_extension_default_icon)
}
@@ -170,7 +170,7 @@ class WebExtensionToolbarTest {
assertFalse(action.iconJob?.isCancelled!!)
attachListenerCaptor.value.onViewDetachedFromWindow(parent)
- testDispatcher.advanceUntilIdle()
+ testDispatcher.scheduler.advanceUntilIdle()
assertTrue(action.iconJob?.isCancelled!!)
}
}
diff --git a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/internal/URLRendererTest.kt b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/internal/URLRendererTest.kt
index 7bbf2587ccf..2aef8ff9cdc 100644
--- a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/internal/URLRendererTest.kt
+++ b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/internal/URLRendererTest.kt
@@ -9,23 +9,20 @@ import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
import mozilla.components.concept.toolbar.Toolbar
import mozilla.components.feature.toolbar.ToolbarFeature
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
-import org.junit.After
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
-import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify
@@ -33,18 +30,8 @@ import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class URLRendererTest {
- @Before
- @ExperimentalCoroutinesApi
- fun setUp() {
- // Execute main thread coroutines on same thread as caller.
- Dispatchers.setMain(Dispatchers.Unconfined)
- }
-
- @After
- @ExperimentalCoroutinesApi
- fun tearDown() {
- Dispatchers.resetMain()
- }
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
@Test
fun `Lifecycle methods start and stop job`() {
@@ -65,7 +52,7 @@ class URLRendererTest {
@Test
fun `Render with configuration`() {
- runBlocking {
+ runTestOnMain {
val configuration = ToolbarFeature.UrlRenderConfiguration(
publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
registrableDomainColor = Color.RED,
diff --git a/components/feature/top-sites/build.gradle b/components/feature/top-sites/build.gradle
index 85a43687eac..e5a7bc7809e 100644
--- a/components/feature/top-sites/build.gradle
+++ b/components/feature/top-sites/build.gradle
@@ -75,6 +75,7 @@ dependencies {
androidTestImplementation Dependencies.androidx_test_core
androidTestImplementation Dependencies.androidx_test_runner
androidTestImplementation Dependencies.androidx_test_rules
+ androidTestImplementation Dependencies.testing_coroutines
}
apply from: '../../../publish.gradle'
diff --git a/components/feature/top-sites/src/androidTest/java/mozilla/components/feature/top/sites/OnDevicePinnedSitesStorageTest.kt b/components/feature/top-sites/src/androidTest/java/mozilla/components/feature/top/sites/OnDevicePinnedSitesStorageTest.kt
index 509a610b897..44fe82621ee 100644
--- a/components/feature/top-sites/src/androidTest/java/mozilla/components/feature/top/sites/OnDevicePinnedSitesStorageTest.kt
+++ b/components/feature/top-sites/src/androidTest/java/mozilla/components/feature/top/sites/OnDevicePinnedSitesStorageTest.kt
@@ -11,7 +11,8 @@ import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.feature.top.sites.db.Migrations
import mozilla.components.feature.top.sites.db.TopSiteDatabase
import org.junit.After
@@ -26,6 +27,7 @@ import java.util.concurrent.Executors
private const val MIGRATION_TEST_DB = "migration-test"
+@ExperimentalCoroutinesApi // for runTest
@Suppress("LargeClass")
class OnDevicePinnedSitesStorageTest {
private lateinit var context: Context
@@ -60,7 +62,7 @@ class OnDevicePinnedSitesStorageTest {
}
@Test
- fun testAddingAllDefaultSites() = runBlocking {
+ fun testAddingAllDefaultSites() = runTest {
val defaultTopSites = listOf(
Pair("Mozilla", "https://www.mozilla.org"),
Pair("Firefox", "https://www.firefox.com"),
@@ -90,7 +92,7 @@ class OnDevicePinnedSitesStorageTest {
}
@Test
- fun testAddingPinnedSite() = runBlocking {
+ fun testAddingPinnedSite() = runTest {
storage.addPinnedSite("Mozilla", "https://www.mozilla.org")
storage.addPinnedSite("Firefox", "https://www.firefox.com", isDefault = true)
@@ -108,7 +110,7 @@ class OnDevicePinnedSitesStorageTest {
}
@Test
- fun testRemovingPinnedSites() = runBlocking {
+ fun testRemovingPinnedSites() = runTest {
storage.addPinnedSite("Mozilla", "https://www.mozilla.org")
storage.addPinnedSite("Firefox", "https://www.firefox.com")
@@ -129,7 +131,7 @@ class OnDevicePinnedSitesStorageTest {
}
@Test
- fun testGettingPinnedSites() = runBlocking {
+ fun testGettingPinnedSites() = runTest {
storage.addPinnedSite("Mozilla", "https://www.mozilla.org")
storage.addPinnedSite("Firefox", "https://www.firefox.com", isDefault = true)
@@ -153,7 +155,7 @@ class OnDevicePinnedSitesStorageTest {
}
@Test
- fun testUpdatingPinnedSites() = runBlocking {
+ fun testUpdatingPinnedSites() = runTest {
storage.addPinnedSite("Mozilla", "https://www.mozilla.org")
var pinnedSites = storage.getPinnedSites()
diff --git a/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/DefaultTopSitesStorage.kt b/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/DefaultTopSitesStorage.kt
index b8dc6a2fdaf..6e87e2b06b0 100644
--- a/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/DefaultTopSitesStorage.kt
+++ b/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/DefaultTopSitesStorage.kt
@@ -102,6 +102,7 @@ class DefaultTopSitesStorage(
try {
providerTopSites = topSitesProvider
.getTopSites(allowCache = true)
+ .filter { providerConfig.providerFilter?.invoke(it) ?: true }
.take(numSitesRequired)
.take(providerConfig.maxThreshold - pinnedSites.size)
topSites.addAll(providerTopSites)
diff --git a/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesConfig.kt b/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesConfig.kt
index a3a10a9fbf5..bffff0a66f3 100644
--- a/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesConfig.kt
+++ b/components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesConfig.kt
@@ -29,8 +29,10 @@ data class TopSitesConfig(
* @property showProviderTopSites Whether or not to display the top sites from the provider.
* @property maxThreshold Only fetch the top sites from the provider if the number of top sites are
* below the maximum threshold.
+ * @property providerFilter Optional function used to filter the top sites from the provider.
*/
data class TopSitesProviderConfig(
val showProviderTopSites: Boolean,
- val maxThreshold: Int = Int.MAX_VALUE
+ val maxThreshold: Int = Int.MAX_VALUE,
+ val providerFilter: ((TopSite) -> Boolean)? = null
)
diff --git a/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/DefaultTopSitesStorageTest.kt b/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/DefaultTopSitesStorageTest.kt
index 52bd4a47111..d44f32d10ed 100644
--- a/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/DefaultTopSitesStorageTest.kt
+++ b/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/DefaultTopSitesStorageTest.kt
@@ -6,16 +6,18 @@ package mozilla.components.feature.top.sites
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
import mozilla.components.concept.storage.FrecencyThresholdOption
import mozilla.components.concept.storage.TopFrecentSiteInfo
import mozilla.components.feature.top.sites.ext.toTopSite
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import mozilla.components.support.test.whenever
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.anyInt
@@ -26,12 +28,15 @@ import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class DefaultTopSitesStorageTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
private val pinnedSitesStorage: PinnedSiteStorage = mock()
private val historyStorage: PlacesHistoryStorage = mock()
private val topSitesProvider: TopSitesProvider = mock()
@Test
- fun `default top sites are added to pinned site storage on init`() = runBlockingTest {
+ fun `default top sites are added to pinned site storage on init`() = runTestOnMain {
val defaultTopSites = listOf(
Pair("Mozilla", "https://mozilla.com"),
Pair("Firefox", "https://firefox.com")
@@ -48,7 +53,7 @@ class DefaultTopSitesStorageTest {
}
@Test
- fun `addPinnedSite`() = runBlockingTest {
+ fun `addPinnedSite`() = runTestOnMain {
val defaultTopSitesStorage = DefaultTopSitesStorage(
pinnedSitesStorage = pinnedSitesStorage,
historyStorage = historyStorage,
@@ -66,7 +71,7 @@ class DefaultTopSitesStorageTest {
}
@Test
- fun `removeTopSite`() = runBlockingTest {
+ fun `removeTopSite`() = runTestOnMain {
val defaultTopSitesStorage = DefaultTopSitesStorage(
pinnedSitesStorage = pinnedSitesStorage,
historyStorage = historyStorage,
@@ -111,7 +116,7 @@ class DefaultTopSitesStorageTest {
}
@Test
- fun `updateTopSite`() = runBlockingTest {
+ fun `updateTopSite`() = runTestOnMain {
val defaultTopSitesStorage = DefaultTopSitesStorage(
pinnedSitesStorage = pinnedSitesStorage,
historyStorage = historyStorage,
@@ -154,7 +159,7 @@ class DefaultTopSitesStorageTest {
}
@Test
- fun `GIVEN frecencyConfig and providerConfig are null WHEN getTopSites is called THEN only default and pinned sites are returned`() = runBlockingTest {
+ fun `GIVEN frecencyConfig and providerConfig are null WHEN getTopSites is called THEN only default and pinned sites are returned`() = runTestOnMain {
val defaultTopSitesStorage = DefaultTopSitesStorage(
pinnedSitesStorage = pinnedSitesStorage,
historyStorage = historyStorage,
@@ -210,7 +215,7 @@ class DefaultTopSitesStorageTest {
}
@Test
- fun `GIVEN providerConfig is specified WHEN getTopSites is called THEN default, pinned and provided top sites are returned`() = runBlockingTest {
+ fun `GIVEN providerConfig is specified WHEN getTopSites is called THEN default, pinned and provided top sites are returned`() = runTestOnMain {
val defaultTopSitesStorage = DefaultTopSitesStorage(
pinnedSitesStorage = pinnedSitesStorage,
historyStorage = historyStorage,
@@ -331,7 +336,7 @@ class DefaultTopSitesStorageTest {
}
@Test
- fun `GIVEN providerConfig with maxThreshold is specified WHEN getTopSites is called THEN the correct number of provided top sites are returned`() = runBlockingTest {
+ fun `GIVEN providerConfig with maxThreshold is specified WHEN getTopSites is called THEN the correct number of provided top sites are returned`() = runTestOnMain {
val defaultTopSitesStorage = DefaultTopSitesStorage(
pinnedSitesStorage = pinnedSitesStorage,
historyStorage = historyStorage,
@@ -473,7 +478,7 @@ class DefaultTopSitesStorageTest {
}
@Test
- fun `GIVEN frecencyConfig and providerConfig are specified WHEN getTopSites is called THEN default, pinned, provided and frecent top sites are returned`() = runBlockingTest {
+ fun `GIVEN frecencyConfig and providerConfig are specified WHEN getTopSites is called THEN default, pinned, provided and frecent top sites are returned`() = runTestOnMain {
val defaultTopSitesStorage = DefaultTopSitesStorage(
pinnedSitesStorage = pinnedSitesStorage,
historyStorage = historyStorage,
@@ -581,7 +586,7 @@ class DefaultTopSitesStorageTest {
}
@Test
- fun `getTopSites returns pinned and frecent sites when frecencyConfig is specified`() = runBlockingTest {
+ fun `getTopSites returns pinned and frecent sites when frecencyConfig is specified`() = runTestOnMain {
val defaultTopSitesStorage = DefaultTopSitesStorage(
pinnedSitesStorage = pinnedSitesStorage,
historyStorage = historyStorage,
@@ -698,7 +703,7 @@ class DefaultTopSitesStorageTest {
}
@Test
- fun `getTopSites filters out frecent sites that already exist in pinned sites`() = runBlockingTest {
+ fun `getTopSites filters out frecent sites that already exist in pinned sites`() = runTestOnMain {
val defaultTopSitesStorage = DefaultTopSitesStorage(
pinnedSitesStorage = pinnedSitesStorage,
historyStorage = historyStorage,
@@ -766,7 +771,91 @@ class DefaultTopSitesStorageTest {
}
@Test
- fun `GIVEN frecent top sites exist as a pinned or provided site WHEN top sites are retrieved THEN filters out frecent sites that already exist in pinned or provided sites`() = runBlockingTest {
+ fun `GIVEN providerFilter is set WHEN getTopSites is called THEN the provided top sites are filtered`() = runTestOnMain {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage = pinnedSitesStorage,
+ historyStorage = historyStorage,
+ topSitesProvider = topSitesProvider,
+ coroutineContext = coroutineContext
+ )
+
+ val filteredUrl = "https://test.com"
+
+ val providerConfig = TopSitesProviderConfig(
+ showProviderTopSites = true,
+ providerFilter = { topSite -> topSite.url != filteredUrl }
+ )
+
+ val defaultSite = TopSite.Default(
+ id = 1,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 1
+ )
+ val pinnedSite = TopSite.Pinned(
+ id = 2,
+ title = "Test",
+ url = filteredUrl,
+ createdAt = 2
+ )
+ val providedSite = TopSite.Provided(
+ id = 3,
+ title = "Mozilla",
+ url = "https://mozilla.com",
+ clickUrl = "https://mozilla.com/click",
+ imageUrl = "https://test.com/image2.jpg",
+ impressionUrl = "https://example.com",
+ createdAt = 3
+ )
+ val providedFilteredSite = TopSite.Provided(
+ id = 3,
+ title = "Filtered",
+ url = filteredUrl,
+ clickUrl = "https://test.com/click",
+ imageUrl = "https://test.com/image2.jpg",
+ impressionUrl = "https://example.com",
+ createdAt = 3
+ )
+
+ whenever(pinnedSitesStorage.getPinnedSites()).thenReturn(
+ listOf(
+ defaultSite,
+ pinnedSite
+ )
+ )
+ whenever(topSitesProvider.getTopSites()).thenReturn(listOf(providedSite, providedFilteredSite))
+
+ val frecentSite1 = TopFrecentSiteInfo("https://getpocket.com", "Pocket")
+ whenever(historyStorage.getTopFrecentSites(anyInt(), any())).thenReturn(listOf(frecentSite1))
+
+ var topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 3,
+ frecencyConfig = FrecencyThresholdOption.NONE,
+ providerConfig = providerConfig
+ )
+
+ assertEquals(3, topSites.size)
+ assertEquals(providedSite, topSites[0])
+ assertEquals(defaultSite, topSites[1])
+ assertEquals(pinnedSite, topSites[2])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+
+ topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 4,
+ frecencyConfig = FrecencyThresholdOption.NONE,
+ providerConfig = providerConfig
+ )
+
+ assertEquals(4, topSites.size)
+ assertEquals(providedSite, topSites[0])
+ assertEquals(defaultSite, topSites[1])
+ assertEquals(pinnedSite, topSites[2])
+ assertEquals(frecentSite1.toTopSite(), topSites[3])
+ assertEquals(defaultTopSitesStorage.cachedTopSites, topSites)
+ }
+
+ @Test
+ fun `GIVEN frecent top sites exist as a pinned or provided site WHEN top sites are retrieved THEN filters out frecent sites that already exist in pinned or provided sites`() = runTestOnMain {
val defaultTopSitesStorage = DefaultTopSitesStorage(
pinnedSitesStorage = pinnedSitesStorage,
historyStorage = historyStorage,
diff --git a/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/PinnedSitesStorageTest.kt b/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/PinnedSitesStorageTest.kt
index d0bdd52faf5..c0d556c7ef4 100644
--- a/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/PinnedSitesStorageTest.kt
+++ b/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/PinnedSitesStorageTest.kt
@@ -4,7 +4,8 @@
package mozilla.components.feature.top.sites
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.feature.top.sites.db.PinnedSiteDao
import mozilla.components.feature.top.sites.db.PinnedSiteEntity
import mozilla.components.feature.top.sites.db.TopSiteDatabase
@@ -15,10 +16,11 @@ import org.junit.Test
import org.mockito.Mockito.`when`
import org.mockito.Mockito.verify
+@ExperimentalCoroutinesApi // for runTest
class PinnedSitesStorageTest {
@Test
- fun addAllDefaultSites() = runBlocking {
+ fun addAllDefaultSites() = runTest {
val storage = PinnedSiteStorage(mock())
val dao = mockDao(storage)
@@ -47,7 +49,7 @@ class PinnedSitesStorageTest {
}
@Test
- fun addPinnedSite() = runBlocking {
+ fun addPinnedSite() = runTest {
val storage = PinnedSiteStorage(mock())
val dao = mockDao(storage)
@@ -66,7 +68,7 @@ class PinnedSitesStorageTest {
}
@Test
- fun removePinnedSite() = runBlocking {
+ fun removePinnedSite() = runTest {
val storage = PinnedSiteStorage(mock())
val dao = mockDao(storage)
@@ -78,7 +80,7 @@ class PinnedSitesStorageTest {
}
@Test
- fun getPinnedSites() = runBlocking {
+ fun getPinnedSites() = runTest {
val storage = PinnedSiteStorage(mock())
val dao = mockDao(storage)
@@ -113,7 +115,7 @@ class PinnedSitesStorageTest {
}
@Test
- fun updatePinnedSite() = runBlocking {
+ fun updatePinnedSite() = runTest {
val storage = PinnedSiteStorage(mock())
val dao = mockDao(storage)
diff --git a/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/TopSitesUseCasesTest.kt b/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/TopSitesUseCasesTest.kt
index 66866bdce14..92f79fec7d1 100644
--- a/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/TopSitesUseCasesTest.kt
+++ b/components/feature/top-sites/src/test/java/mozilla/components/feature/top/sites/TopSitesUseCasesTest.kt
@@ -5,17 +5,19 @@
package mozilla.components.feature.top.sites
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.support.test.mock
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class TopSitesUseCasesTest {
@Test
- fun `AddPinnedSiteUseCase`() = runBlocking {
+ fun `AddPinnedSiteUseCase`() = runTest {
val topSitesStorage: TopSitesStorage = mock()
val useCases = TopSitesUseCases(topSitesStorage)
@@ -28,7 +30,7 @@ class TopSitesUseCasesTest {
}
@Test
- fun `RemoveTopSiteUseCase`() = runBlocking {
+ fun `RemoveTopSiteUseCase`() = runTest {
val topSitesStorage: TopSitesStorage = mock()
val topSite: TopSite = mock()
val useCases = TopSitesUseCases(topSitesStorage)
@@ -39,7 +41,7 @@ class TopSitesUseCasesTest {
}
@Test
- fun `UpdateTopSiteUseCase`() = runBlocking {
+ fun `UpdateTopSiteUseCase`() = runTest {
val topSitesStorage: TopSitesStorage = mock()
val topSite: TopSite = mock()
val useCases = TopSitesUseCases(topSitesStorage)
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/data/injections.js b/components/feature/webcompat/src/main/assets/extensions/webcompat/data/injections.js
index a90690b240c..52049ac9fac 100644
--- a/components/feature/webcompat/src/main/assets/extensions/webcompat/data/injections.js
+++ b/components/feature/webcompat/src/main/assets/extensions/webcompat/data/injections.js
@@ -122,16 +122,16 @@ const AVAILABLE_INJECTIONS = [
customFunc: "noSniffFix",
},
{
- id: "bug1561371",
- platform: "android",
- domain: "mail.google.com",
- bug: "1561371",
+ id: "bug1768243",
+ platform: "desktop",
+ domain: "cloud.google.com",
+ bug: "1768243",
contentScripts: {
- matches: ["*://mail.google.com/*"],
+ matches: ["*://cloud.google.com/terms/*"],
css: [
{
file:
- "injections/css/bug1561371-mail.google.com-allow-horizontal-scrolling.css",
+ "injections/css/bug1768243-cloud.google.com-allow-table-scrolling.css",
},
],
},
@@ -318,21 +318,6 @@ const AVAILABLE_INJECTIONS = [
],
},
},
- {
- id: "bug1756054",
- platform: "all",
- domain: "tataplayrecharge.com",
- bug: "1756054",
- contentScripts: {
- matches: ["*://www.tataplayrecharge.com/*"],
- css: [
- {
- file:
- "injections/css/bug1756054-tataplayrecharge.com-clear-float.css",
- },
- ],
- },
- },
{
id: "bug1731825",
platform: "desktop",
@@ -403,20 +388,6 @@ const AVAILABLE_INJECTIONS = [
],
},
},
- {
- id: "bug1719870",
- platform: "all",
- domain: "lcbo.com",
- bug: "1719870",
- contentScripts: {
- matches: ["*://*.lcbo.com/*"],
- css: [
- {
- file: "injections/css/bug1719870-lcbo.com-table-clearfix.css",
- },
- ],
- },
- },
{
id: "bug1722955",
platform: "android",
@@ -464,35 +435,6 @@ const AVAILABLE_INJECTIONS = [
allFrames: true,
},
},
- {
- id: "bug1727080",
- platform: "android",
- domain: "nexity.fr",
- bug: "1727080",
- contentScripts: {
- matches: ["*://*.nexity.fr/*"],
- css: [
- {
- file: "injections/css/bug1727080-nexity.fr-svg-size-fix.css",
- },
- ],
- },
- },
- {
- id: "bug1738313",
- platform: "desktop",
- domain: "curriculum.gov.bc.ca",
- bug: "1738313",
- contentScripts: {
- matches: ["*://curriculum.gov.bc.ca/*"],
- css: [
- {
- file:
- "injections/css/bug1738313-curriculum.gov.bc.ca-bootstrap-fix.css",
- },
- ],
- },
- },
{
id: "bug1741234",
platform: "all",
@@ -580,30 +522,58 @@ const AVAILABLE_INJECTIONS = [
},
},
{
- id: "bug1756915",
+ id: "bug1739489",
platform: "desktop",
- domain: "efectococuyo.com",
- bug: "1756915",
+ domain: "draft.js",
+ bug: "1739489",
+ contentScripts: {
+ matches: ["*://draftjs.org/*", "*://www.facebook.com/*"],
+ js: [
+ {
+ file: "injections/js/bug1739489-draftjs-beforeinput.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1765947",
+ platform: "android",
+ domain: "veniceincoming.com",
+ bug: "1765947",
contentScripts: {
- matches: ["*://efectococuyo.com/*"],
+ matches: ["*://veniceincoming.com/*"],
css: [
{
- file:
- "injections/css/bug1756915-efectococuyo.com-shifted-content-fix.css",
+ file: "injections/css/bug1765947-veniceincoming.com-left-fix.css",
},
],
},
},
{
- id: "bug1739489",
- platform: "desktop",
- domain: "draft.js",
- bug: "1739489",
+ id: "bug11769762",
+ platform: "all",
+ domain: "tiktok.com",
+ bug: "1769762",
contentScripts: {
- matches: ["*://draftjs.org/*", "*://www.facebook.com/*"],
+ matches: ["https://www.tiktok.com/*"],
js: [
{
- file: "injections/js/bug1739489-draftjs-beforeinput.js",
+ file: "injections/js/bug1769762-tiktok.com-plugins-shim.js",
+ },
+ ],
+ },
+ },
+ {
+ id: "bug1770962",
+ platform: "all",
+ domain: "coldwellbankerhomes.com",
+ bug: "1770962",
+ contentScripts: {
+ matches: ["*://*.coldwellbankerhomes.com/*"],
+ css: [
+ {
+ file:
+ "injections/css/bug1770962-coldwellbankerhomes.com-image-height.css",
},
],
},
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/data/shims.js b/components/feature/webcompat/src/main/assets/extensions/webcompat/data/shims.js
index bc92d4c008a..321bbfade22 100644
--- a/components/feature/webcompat/src/main/assets/extensions/webcompat/data/shims.js
+++ b/components/feature/webcompat/src/main/assets/extensions/webcompat/data/shims.js
@@ -397,7 +397,10 @@ const AVAILABLE_SHIMS = [
name: "Integral Ad Science PET",
bug: "1713701",
file: "iaspet.js",
- matches: ["*://cdn.adsafeprotected.com/iasPET.1.js"],
+ matches: [
+ "*://cdn.adsafeprotected.com/iasPET.1.js",
+ "*://static.adsafeprotected.com/iasPET.1.js",
+ ],
onlyIfBlockedByETP: true,
},
{
@@ -608,6 +611,19 @@ const AVAILABLE_SHIMS = [
matches: ["*://js.maxmind.com/js/apis/geoip2/*/geoip2.js"],
onlyIfBlockedByETP: true,
},
+ {
+ id: "WebTrends",
+ platform: "all",
+ name: "WebTrends",
+ bug: "1766414",
+ file: "webtrends.js",
+ matches: [
+ "*://s.webtrends.com/js/advancedLinkTracking.js",
+ "*://s.webtrends.com/js/webtrends.js",
+ "*://s.webtrends.com/js/webtrends.min.js",
+ ],
+ onlyIfBlockedByETP: true,
+ },
];
module.exports = AVAILABLE_SHIMS;
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/data/ua_overrides.js b/components/feature/webcompat/src/main/assets/extensions/webcompat/data/ua_overrides.js
index c84baf622b8..c5614c5556d 100644
--- a/components/feature/webcompat/src/main/assets/extensions/webcompat/data/ua_overrides.js
+++ b/components/feature/webcompat/src/main/assets/extensions/webcompat/data/ua_overrides.js
@@ -714,16 +714,19 @@ const AVAILABLE_UA_OVERRIDES = [
bug: "1743429",
config: {
matches: [
+ "*://*.commerzbank.de/*", // Bug 1767630
"*://*.edf.com/*", // Bug 1764786
"*://*.wordpress.org/*", // Bug 1743431
- "*://bethesda.net/*", // #94607
+ "*://as.eservice.asus.com/*", // #104113
+ "*://bethesda.net/*", // #94607,
"*://cdn-vzn.yottaa.net/*", // Bug 1764795
- "*://citoyens.telerecours.fr/*", // #101066
- "*://genehmigung.ahs-vwa.at/*", // #100063
+ "*://dsae.co.za/*", // Bug 1765925
+ "*://fpt.dfp.microsoft.com/*", // #104237
"*://moje.pzu.pl/*", // #99772
"*://mon.allianzbanque.fr/*", // #101074
"*://online.citi.com/*", // #101268
"*://simperium.com/*", // #98934
+ "*://ubank.com.au/*", // #104099
"*://wifi.sncf/*", // #100194
"*://www.accringtonobserver.co.uk/*", // Bug 1762928 (Reach Plc)
"*://www.bathchronicle.co.uk/*", // Bug 1762928 (Reach Plc)
@@ -782,6 +785,7 @@ const AVAILABLE_UA_OVERRIDES = [
"*://www.irishmirror.ie/*", // Bug 1762928 (Reach Plc)
"*://www.kentlive.news/*", // Bug 1762928 (Reach Plc)
"*://www.lancs.live/*", // Bug 1762928 (Reach Plc)
+ "*://www.learningants.com/*", // #104080
"*://www.leeds-live.co.uk/*", // Bug 1762928 (Reach Plc)
"*://www.leicestermercury.co.uk/*", // Bug 1762928 (Reach Plc)
"*://www.lincolnshirelive.co.uk/*", // Bug 1762928 (Reach Plc)
@@ -791,12 +795,10 @@ const AVAILABLE_UA_OVERRIDES = [
"*://www.macclesfield-express.co.uk/*", // Bug 1762928 (Reach Plc)
"*://www.macclesfield-live.co.uk/*", // Bug 1762928 (Reach Plc)
"*://www.manchestereveningnews.co.uk/*", // #100923
- "*://www.mirror.co.uk/*", // #100053
"*://www.mylondon.news/*", // Bug 1762928 (Reach Plc)
"*://www.northantslive.news/*", // Bug 1762928 (Reach Plc)
"*://www.nottinghampost.com/*", // Bug 1762928 (Reach Plc)
"*://www.ok.co.uk/*", // Bug 1762928 (Reach Plc)
- "*://www.petalmail.com/*", // #99339
"*://www.plymouthherald.co.uk/*", // Bug 1762928 (Reach Plc)
"*://www.rossendalefreepress.co.uk/*", // Bug 1762928 (Reach Plc)
"*://www.rsvplive.ie/*", // Bug 1762928 (Reach Plc)
@@ -809,9 +811,9 @@ const AVAILABLE_UA_OVERRIDES = [
"*://www.southportvisiter.co.uk/*", // Bug 1762928 (Reach Plc)
"*://www.staffordshire-live.co.uk/*", // Bug 1762928 (Reach Plc)
"*://www.stokesentinel.co.uk/*", // Bug 1762928 (Reach Plc)
+ "*://survey.sogosurvey.com/*", // Bug 1765925
"*://www.sussexlive.co.uk/*", // Bug 1762928 (Reach Plc)
"*://www.tm-awx.com/*", // Bug 1762928 (Reach Plc)
- "*://www.twitch.tv/*", // Bug 1764591
"*://www.walesonline.co.uk/*", // Bug 1762928 (Reach Plc)
"*://www.wharf.co.uk/*", // Bug 1762928 (Reach Plc)
],
@@ -820,38 +822,6 @@ const AVAILABLE_UA_OVERRIDES = [
},
},
},
- {
- /*
- * Bug 1751232 - Add override for sites returning desktop layout for Android 12
- * Webcompat issue #92978 - https://github.com/webcompat/web-bugs/issues/92978
- *
- * A number of news sites returns desktop layout for Android 12 only. Changing it
- * to Android 12.0 fixes the issue
- */
- id: "bug1751232",
- platform: "android",
- domain: "Sites with desktop layout for Android 12",
- bug: "1751232",
- config: {
- matches: [
- "*://*.dw.com/*",
- "*://*.abc10.com/*",
- "*://*.wnep.com/*",
- "*://*.dn.se/*",
- "*://*.dailymail.co.uk/*",
- "*://*.kohls.com/*",
- "*://*.expressen.se/*",
- "*://*.walmart.com/*",
- ],
- uaTransformer: originalUA => {
- if (!originalUA.includes("Android 12;")) {
- return originalUA;
- }
-
- return originalUA.replace("Android 12;", "Android 12.0;");
- },
- },
- },
{
/*
* Bug 1754180 - UA override for nordjyske.dk
@@ -871,25 +841,6 @@ const AVAILABLE_UA_OVERRIDES = [
},
},
},
- {
- /*
- * Bug 1177298 - UA overrides for expertflyer.com
- * Webcompat issue #96685 - https://webcompat.com/issues/96685
- *
- * The site does not offer a stylesheet unless AppleWebKit
- * is part of the user-agent string.
- */
- id: "bug1753631",
- platform: "android",
- domain: "expertflyer.com",
- bug: "1753631",
- config: {
- matches: ["*://*.expertflyer.com/*"],
- uaTransformer: originalUA => {
- return originalUA + " AppleWebKit";
- },
- },
- },
{
/*
* Bug 1753461 - UA override for serieson.naver.com
@@ -945,6 +896,25 @@ const AVAILABLE_UA_OVERRIDES = [
},
},
},
+ {
+ /*
+ * Bug 1771200 - UA override for animalplanet.com
+ * Webcompat issue #99993 - https://webcompat.com/issues/103727
+ *
+ * The videos are not playing and an error message is displayed
+ * in Firefox for Android, but work with Chrome UA
+ */
+ id: "bug1771200",
+ platform: "android",
+ domain: "animalplanet.com",
+ bug: "1771200",
+ config: {
+ matches: ["*://*.animalplanet.com/video/*"],
+ uaTransformer: originalUA => {
+ return UAHelpers.getDeviceAppropriateChromeUA();
+ },
+ },
+ },
];
module.exports = AVAILABLE_UA_OVERRIDES;
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/trackingProtection.js b/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/trackingProtection.js
index e2083ba07bc..f2f30410f7a 100644
--- a/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/trackingProtection.js
+++ b/components/feature/webcompat/src/main/assets/extensions/webcompat/experiment-apis/trackingProtection.js
@@ -22,7 +22,7 @@ class AllowList {
setAllows(patterns, hosts) {
this._allowPatterns = patterns;
- this._allowMatcher = new MatchPatternSet(patterns) || [];
+ this._allowMatcher = new MatchPatternSet(patterns || []);
this._allowHosts = hosts || [];
return this;
}
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1561371-mail.google.com-allow-horizontal-scrolling.css b/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1561371-mail.google.com-allow-horizontal-scrolling.css
deleted file mode 100644
index 15a7fe14841..00000000000
--- a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1561371-mail.google.com-allow-horizontal-scrolling.css
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * mail.google.com - The HTML email view does not allow horizontal scrolling
- * on Firefox mobile due to a missing CSS rule which is only served to Chrome.
- * Bug #1561371 - https://bugzilla.mozilla.org/show_bug.cgi?id=1561371
- *
- * HTML emails may sometimes contain content that does not wrap, yet the
- * CSS served to Firefox Mobile does not permit scrolling horizontally.
- * To prevent this UX frustration, we enable horizontal scrolling.
- */
-body > #views {
- overflow: auto;
-}
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1719870-lcbo.com-table-clearfix.css b/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1719870-lcbo.com-table-clearfix.css
deleted file mode 100644
index a7fc326dde6..00000000000
--- a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1719870-lcbo.com-table-clearfix.css
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * lcbo.com - Table with locations is not showing.
- * Bug #1719870 - https://bugzilla.mozilla.org/show_bug.cgi?id=1719870
- * WebCompat issue #75137 - https://webcompat.com/issues/75137
- */
-
-.physicalStoreInventoryPage .scrollX {
- clear: both;
-}
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1727080-nexity.fr-svg-size-fix.css b/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1727080-nexity.fr-svg-size-fix.css
deleted file mode 100644
index cad34451438..00000000000
--- a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1727080-nexity.fr-svg-size-fix.css
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * nexity.fr - Overlapping elements prevent page navigation.
- * Bug #1727080 - https://bugzilla.mozilla.org/show_bug.cgi?id=1727080
- * WebCompat issue #75869 - https://webcompat.com/issues/75869
- */
-
-.sub-myspace li svg {
- width: 15px;
-}
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1738313-curriculum.gov.bc.ca-bootstrap-fix.css b/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1738313-curriculum.gov.bc.ca-bootstrap-fix.css
deleted file mode 100644
index a46a0ec27c4..00000000000
--- a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1738313-curriculum.gov.bc.ca-bootstrap-fix.css
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * curriculum.gov.bc.ca - Page layout is broken.
- * Bug #1738313 - https://bugzilla.mozilla.org/show_bug.cgi?id=1738313
- * WebCompat issue #90988 - https://webcompat.com/issues/90988
- */
-
-.row {
- margin-right: 0;
- margin-left: 0;
- width: calc(100% - 30px);
-}
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1756054-tataplayrecharge.com-clear-float.css b/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1756054-tataplayrecharge.com-clear-float.css
deleted file mode 100644
index b018a1e6036..00000000000
--- a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1756054-tataplayrecharge.com-clear-float.css
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * tataplayrecharge.com - Content of the form is shifted to the right
- * Bug #1756054 - https://bugzilla.mozilla.org/show_bug.cgi?id=1756054
- * WebCompat issue #61521 - https://webcompat.com/issues/61521
- *
- * The form is shifted off screen due to combination of negative
- * margins and float, which is https://bugzilla.mozilla.org/show_bug.cgi?id=1400958
- * Clearing the float fixes the issue
- */
-.modal-body .InfoMsg + div {
- clear: both;
-}
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1756915-efectococuyo.com-shifted-content-fix.css b/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1756915-efectococuyo.com-shifted-content-fix.css
deleted file mode 100644
index bb209272967..00000000000
--- a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1756915-efectococuyo.com-shifted-content-fix.css
+++ /dev/null
@@ -1,8 +0,0 @@
-/**
- * efectococuyo.com - content is shifted off-screen due
- * to using floats and negative margins
- * Bug #1756915 - https://bugzilla.mozilla.org/show_bug.cgi?id=1756915
- */
-.code-block + .row {
- clear: both;
-}
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1765947-veniceincoming.com-left-fix.css b/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1765947-veniceincoming.com-left-fix.css
new file mode 100644
index 00000000000..4c2df651ba0
--- /dev/null
+++ b/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1765947-veniceincoming.com-left-fix.css
@@ -0,0 +1,9 @@
+/**
+ * veniceincoming.com - site is not usable
+ * Bug #1765947 - https://bugzilla.mozilla.org/show_bug.cgi?id=1765947
+ * WebCompat issue #102133 - https://webcompat.com/issues/102133
+ */
+
+.tour-list .single-tour .mobile-link {
+ left: 0;
+}
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1768243-cloud.google.com-allow-table-scrolling.css b/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1768243-cloud.google.com-allow-table-scrolling.css
new file mode 100644
index 00000000000..aff9ea257af
--- /dev/null
+++ b/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1768243-cloud.google.com-allow-table-scrolling.css
@@ -0,0 +1,9 @@
+/**
+ * cloud.google.com - The table doesn't allow horizontal scrolling
+ * Bug #1768243 - https://bugzilla.mozilla.org/show_bug.cgi?id=1768243
+ * WebCompat issue #103328 - https://webcompat.com/issues/103328
+ */
+
+.cloud-collapse__panel .cws-grid__col--span-12 {
+ min-width: 0;
+}
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1770962-coldwellbankerhomes.com-image-height.css b/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1770962-coldwellbankerhomes.com-image-height.css
new file mode 100644
index 00000000000..22a6b331d1d
--- /dev/null
+++ b/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/css/bug1770962-coldwellbankerhomes.com-image-height.css
@@ -0,0 +1,14 @@
+/**
+ * coldwellbankerhomes.com - Property images are displayed squeezed
+ * Bug #1770962 - https://bugzilla.mozilla.org/show_bug.cgi?id=1770962
+ * WebCompat issue #102872 - https://webcompat.com/issues/102872
+ */
+
+.property-snapshot-psr-panel
+ .prop-pix
+ .photo-carousel.owl
+ .owl-stage-outer
+ .owl-item
+ img {
+ height: -moz-available;
+}
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1769762-tiktok.com-plugins-shim.js b/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1769762-tiktok.com-plugins-shim.js
new file mode 100644
index 00000000000..12546c64314
--- /dev/null
+++ b/components/feature/webcompat/src/main/assets/extensions/webcompat/injections/js/bug1769762-tiktok.com-plugins-shim.js
@@ -0,0 +1,31 @@
+"use strict";
+
+/**
+ * Bug 1769762 - Empty out navigator.plugins
+ * WebCompat issue #103612 - https://webcompat.com/issues/103612
+ *
+ * Certain features of the site are breaking if navigator.plugins array is not empty:
+ *
+ * 1. "Likes" on the comments are not saved
+ * 2. Can't reply to other people's comments
+ * 3. "Likes" on the videos are not saved
+ * 4. Can't follow an account (after refreshing "Follow" button is visible again)
+ *
+ * (note that the first 2 are still broken if you open devtools even with this intervention)
+ */
+
+/* globals exportFunction */
+
+console.info(
+ "The PluginArray has been overridden for compatibility reasons. See https://bugzilla.mozilla.org/show_bug.cgi?id=1753874 for details."
+);
+
+const pluginsArray = new window.wrappedJSObject.Array();
+Object.setPrototypeOf(pluginsArray, PluginArray.prototype);
+
+Object.defineProperty(navigator.wrappedJSObject, "plugins", {
+ get: exportFunction(function() {
+ return pluginsArray;
+ }, window),
+ set: exportFunction(function(val) {}, window),
+});
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/manifest.json b/components/feature/webcompat/src/main/assets/extensions/webcompat/manifest.json
index 5167df771bf..7a06c0bebd4 100644
--- a/components/feature/webcompat/src/main/assets/extensions/webcompat/manifest.json
+++ b/components/feature/webcompat/src/main/assets/extensions/webcompat/manifest.json
@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "Mozilla Android Components - Web Compatibility Interventions",
"description": "Urgent post-release fixes for web compatibility.",
- "version": "100.2.0",
+ "version": "102.0.0",
"applications": {
"gecko": {
"id": "webcompat@mozilla.org",
@@ -64,6 +64,7 @@
"content_security_policy": "script-src 'self' 'sha256-MmZkN2QaIHhfRWPZ8TVRjijTn5Ci1iEabtTEWrt9CCo='; default-src 'self'; base-uri moz-extension://*; object-src 'none'",
"permissions": [
+ "mozillaAddons",
"tabs",
"webNavigation",
"webRequest",
@@ -135,6 +136,7 @@
"shims/vast2.xml",
"shims/vast3.xml",
"shims/vidible.js",
- "shims/vmad.xml"
+ "shims/vmad.xml",
+ "shims/webtrends.js"
]
}
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-ima.js b/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-ima.js
index b5cdb5d7120..0909b20cf73 100644
--- a/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-ima.js
+++ b/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-ima.js
@@ -336,6 +336,9 @@ if (!window.google?.ima?.VERSION) {
isLinear() {
return true;
}
+ isSkippable() {
+ return true;
+ }
}
class CompanionAd {
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-publisher-tags.js b/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-publisher-tags.js
index 0a262739056..709c5af3b79 100644
--- a/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-publisher-tags.js
+++ b/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/google-publisher-tags.js
@@ -32,7 +32,7 @@ if (window.googletag?.apiReady === undefined) {
requestAnimationFrame(() => {
const size = [0, 0];
for (const cb of eventCallbacks.get(name) || []) {
- cb({ isEmpty: true, size, slot });
+ cb({ isEmpty: false, size, slot });
}
resolve();
});
@@ -53,11 +53,19 @@ if (window.googletag?.apiReady === undefined) {
}
};
+ const emptySlotElement = slot => {
+ const node = document.getElementById(slot.getSlotElementId());
+ while (node?.lastChild) {
+ node.lastChild.remove();
+ }
+ };
+
const callbackIfSlotReady = async id => {
const slot = slotsById.get(id);
if (!slot || !refreshedSlots.has(id) || !displayedSlots.has(id)) {
return;
}
+ emptySlotElement(slot);
recreateIframeForSlot(slot);
await fireSlotEvent("slotRenderEnded", slot);
await fireSlotEvent("slotRequested", slot);
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/iaspet.js b/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/iaspet.js
index 3622bbec2fc..26fd9f31c5a 100644
--- a/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/iaspet.js
+++ b/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/iaspet.js
@@ -12,12 +12,29 @@
*/
if (!window.__iasPET?.VERSION) {
+ let queue = window?.__iasPET?.queue;
+ if (!Array.isArray(queue)) {
+ queue = [];
+ }
+
+ function run(cmd) {
+ try {
+ cmd?.dataHandler?.();
+ } catch (_) {}
+ }
+
+ queue.push = run;
+
window.__iasPET = {
VERSION: "1.16.18",
- queue: [],
+ queue,
sessionId: "",
setTargetingForAppNexus() {},
setTargetingForGPT() {},
start() {},
};
+
+ while (queue.length) {
+ run(queue.shift());
+ }
}
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/maxmind-geoip.js b/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/maxmind-geoip.js
index 6bfd0d096ce..e5eb1e45a3c 100644
--- a/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/maxmind-geoip.js
+++ b/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/maxmind-geoip.js
@@ -9,13 +9,56 @@
*
* Some sites rely on Maxmind's GeoIP library which gets blocked by ETP's
* fingerprinter blocking. With the library window global not being defined
- * functionality may break or the site does not render at all. This shim adds a
- * dummy object which returns errors for any request to mitigate the breakage.
+ * functionality may break or the site does not render at all. This shim
+ * has it return the United States as the location for all users.
*/
if (!window.geoip2) {
- const callback = (_, onError) => {
- onError("");
+ const continent = {
+ code: "NA",
+ geoname_id: 6255149,
+ names: {
+ de: "Nordamerika",
+ en: "North America",
+ es: "Norteamérica",
+ fr: "Amérique du Nord",
+ ja: "北アメリカ",
+ "pt-BR": "América do Norte",
+ ru: "Северная Америка",
+ "zh-CN": "北美洲",
+ },
+ };
+
+ const country = {
+ geoname_id: 6252001,
+ iso_code: "US",
+ names: {
+ de: "USA",
+ en: "United States",
+ es: "Estados Unidos",
+ fr: "États-Unis",
+ ja: "アメリカ合衆国",
+ "pt-BR": "Estados Unidos",
+ ru: "США",
+ "zh-CN": "美国",
+ },
+ };
+
+ const city = {
+ names: {
+ en: "",
+ },
+ };
+
+ const callback = onSuccess => {
+ requestAnimationFrame(() => {
+ onSuccess({
+ city,
+ continent,
+ country,
+ registered_country: country,
+ });
+ });
};
window.geoip2 = {
diff --git a/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/webtrends.js b/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/webtrends.js
new file mode 100644
index 00000000000..c7ef0069da2
--- /dev/null
+++ b/components/feature/webcompat/src/main/assets/extensions/webcompat/shims/webtrends.js
@@ -0,0 +1,46 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Bug 1766414 - Shim WebTrends Core Tag and Advanced Link Tracking
+ *
+ * Sites using WebTrends Core Tag or Link Tracking can break if they are
+ * are blocked. This shim mitigates that breakage by loading an empty module.
+ */
+
+if (!window.dcsMultiTrack) {
+ window.dcsMultiTrack = o => {
+ o?.callback?.({});
+ };
+}
+
+if (!window.WebTrends) {
+ class dcs {
+ addSelector() {
+ return this;
+ }
+ addTransform() {
+ return this;
+ }
+ DCSext = {};
+ init(obj) {
+ return this;
+ }
+ track() {
+ return this;
+ }
+ }
+
+ window.Webtrends = window.WebTrends = {
+ dcs,
+ multiTrack: window.dcsMultiTrack,
+ };
+
+ window.requestAnimationFrame(() => {
+ window.webtrendsAsyncLoad?.(dcs);
+ window.webtrendsAsyncInit?.();
+ });
+}
diff --git a/components/feature/webnotifications/src/main/res/values-skr/strings.xml b/components/feature/webnotifications/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..ef5f54b5822
--- /dev/null
+++ b/components/feature/webnotifications/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+
+
+
+ سائٹ نوٹیفیکیشن
+
diff --git a/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/NativeNotificationBridgeTest.kt b/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/NativeNotificationBridgeTest.kt
index 98d75314452..bfd6aaec4fc 100644
--- a/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/NativeNotificationBridgeTest.kt
+++ b/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/NativeNotificationBridgeTest.kt
@@ -5,14 +5,18 @@
package mozilla.components.feature.webnotifications
import android.app.Notification
+import android.app.Notification.BigTextStyle
import android.app.Notification.EXTRA_SUB_TEXT
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.icons.Icon
import mozilla.components.browser.icons.IconRequest
+import mozilla.components.browser.icons.IconRequest.Resource
+import mozilla.components.browser.icons.IconRequest.Resource.Type.MANIFEST_ICON
+import mozilla.components.browser.icons.IconRequest.Size.DEFAULT
import mozilla.components.concept.engine.webnotifications.WebNotification
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
@@ -34,8 +38,8 @@ private const val TEST_TEXT = "test text"
private const val TEST_URL = "mozilla.org"
private const val TEST_CHANNEL = "testChannel"
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
-@ExperimentalCoroutinesApi
class NativeNotificationBridgeTest {
private val blankNotification = WebNotification(
TEST_TITLE, TEST_TAG, TEST_TEXT, TEST_URL, null, null,
@@ -55,7 +59,7 @@ class NativeNotificationBridgeTest {
}
@Test
- fun `create blank notification`() = runBlockingTest {
+ fun `create blank notification`() = runTest {
val notification = bridge.convertToAndroidNotification(
blankNotification,
testContext,
@@ -73,7 +77,7 @@ class NativeNotificationBridgeTest {
}
@Test
- fun `set when`() = runBlockingTest {
+ fun `set when`() = runTest {
val notification = bridge.convertToAndroidNotification(
blankNotification.copy(timestamp = 1234567890),
testContext,
@@ -86,7 +90,7 @@ class NativeNotificationBridgeTest {
}
@Test
- fun `icon is loaded from BrowserIcons`() = runBlockingTest {
+ fun `icon is loaded from BrowserIcons`() = runTest {
bridge.convertToAndroidNotification(
blankNotification.copy(sourceUrl = "https://example.com", iconUrl = "https://example.com/large.png"),
testContext,
@@ -98,11 +102,11 @@ class NativeNotificationBridgeTest {
verify(icons).loadIcon(
IconRequest(
url = "https://example.com",
- size = IconRequest.Size.DEFAULT,
+ size = DEFAULT,
resources = listOf(
- IconRequest.Resource(
+ Resource(
url = "https://example.com/large.png",
- type = IconRequest.Resource.Type.MANIFEST_ICON
+ type = MANIFEST_ICON
)
),
isPrivate = true
@@ -111,7 +115,7 @@ class NativeNotificationBridgeTest {
}
@Test
- fun `android notification sets BigTextStyle`() = runBlockingTest {
+ fun `android notification sets BigTextStyle`() = runTest {
val notification = bridge.convertToAndroidNotification(
blankNotification.copy(iconUrl = "https://example.com/large.png"),
testContext,
@@ -120,7 +124,7 @@ class NativeNotificationBridgeTest {
0
)
- val expectedStyle = Notification.BigTextStyle().javaClass.name
+ val expectedStyle = BigTextStyle().javaClass.name
assertEquals(expectedStyle, notification.extras.getString(Notification.EXTRA_TEMPLATE))
val noBodyNotification = bridge.convertToAndroidNotification(
diff --git a/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/WebNotificationFeatureTest.kt b/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/WebNotificationFeatureTest.kt
index 6d3888eff68..6a7bd9d19d2 100644
--- a/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/WebNotificationFeatureTest.kt
+++ b/components/feature/webnotifications/src/test/java/mozilla/components/feature/webnotifications/WebNotificationFeatureTest.kt
@@ -8,7 +8,6 @@ import android.app.NotificationManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.icons.Icon
import mozilla.components.browser.icons.Icon.Source
@@ -21,7 +20,10 @@ import mozilla.components.support.test.any
import mozilla.components.support.test.eq
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.`when`
@@ -34,6 +36,10 @@ import org.mockito.Mockito.verify
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class WebNotificationFeatureTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
private val context = spy(testContext)
private val browserIcons: BrowserIcons = mock()
private val icon: Icon = mock()
@@ -84,7 +90,7 @@ class WebNotificationFeatureTest {
}
@Test
- fun `engine notifies to show notification`() = runBlockingTest {
+ fun `engine notifies to show notification`() = runTestOnMain {
val notification = testNotification.copy(sourceUrl = "https://mozilla.org:443")
val feature = WebNotificationFeature(
context,
@@ -105,7 +111,7 @@ class WebNotificationFeatureTest {
}
@Test
- fun `notification ignored if permissions are not allowed`() = runBlockingTest {
+ fun `notification ignored if permissions are not allowed`() = runTestOnMain {
val notification = testNotification.copy(sourceUrl = "https://mozilla.org:443")
val feature = WebNotificationFeature(
context,
@@ -134,7 +140,7 @@ class WebNotificationFeatureTest {
}
@Test
- fun `notifications always allowed for web extensions`() = runBlockingTest {
+ fun `notifications always allowed for web extensions`() = runTestOnMain {
val webExtensionNotification = WebNotification(
"Mozilla",
"mozilla.org",
diff --git a/components/feature/biometric-prompt/build.gradle b/components/lib/biometric-prompt/build.gradle
similarity index 100%
rename from components/feature/biometric-prompt/build.gradle
rename to components/lib/biometric-prompt/build.gradle
diff --git a/components/feature/biometric-prompt/proguard-rules.pro b/components/lib/biometric-prompt/proguard-rules.pro
similarity index 100%
rename from components/feature/biometric-prompt/proguard-rules.pro
rename to components/lib/biometric-prompt/proguard-rules.pro
diff --git a/components/feature/biometric-prompt/src/main/AndroidManifest.xml b/components/lib/biometric-prompt/src/main/AndroidManifest.xml
similarity index 100%
rename from components/feature/biometric-prompt/src/main/AndroidManifest.xml
rename to components/lib/biometric-prompt/src/main/AndroidManifest.xml
diff --git a/components/lib/biometric-prompt/src/main/java/mozilla.components.lib.auth/AuthenticationCallbacks.kt b/components/lib/biometric-prompt/src/main/java/mozilla.components.lib.auth/AuthenticationCallbacks.kt
new file mode 100644
index 00000000000..651e417b1a3
--- /dev/null
+++ b/components/lib/biometric-prompt/src/main/java/mozilla.components.lib.auth/AuthenticationCallbacks.kt
@@ -0,0 +1,26 @@
+/* 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.lib.auth
+
+/**
+ * Callbacks for BiometricPrompt Authentication
+ */
+interface AuthenticationCallbacks {
+
+ /**
+ * Called when a biometric (e.g. fingerprint, face, etc.) is presented but not recognized as belonging to the user.
+ */
+ val onAuthFailure: () -> Unit
+
+ /**
+ * Called when a biometric (e.g. fingerprint, face, etc.) is recognized, indicating that the user has successfully authenticated.
+ */
+ val onAuthSuccess: () -> Unit
+
+ /**
+ * Called when an unrecoverable error has been encountered and authentication has stopped.
+ */
+ val onAuthError: (errorText: String) -> Unit
+}
diff --git a/components/feature/biometric-prompt/src/main/java/mozilla/components/feature/biometric/BiometricPromptFeature.kt b/components/lib/biometric-prompt/src/main/java/mozilla.components.lib.auth/BiometricPromptFeature.kt
similarity index 67%
rename from components/feature/biometric-prompt/src/main/java/mozilla/components/feature/biometric/BiometricPromptFeature.kt
rename to components/lib/biometric-prompt/src/main/java/mozilla.components.lib.auth/BiometricPromptFeature.kt
index c9f78440fc2..aa1c72165cd 100644
--- a/components/feature/biometric-prompt/src/main/java/mozilla/components/feature/biometric/BiometricPromptFeature.kt
+++ b/components/lib/biometric-prompt/src/main/java/mozilla.components.lib.auth/BiometricPromptFeature.kt
@@ -2,13 +2,10 @@
* 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.feature.biometric
+package mozilla.components.lib.auth
import android.content.Context
-import android.os.Build.VERSION.SDK_INT
-import android.os.Build.VERSION_CODES.M
import androidx.annotation.VisibleForTesting
-import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt
@@ -24,7 +21,6 @@ import mozilla.components.support.base.log.logger.Logger
* @param fragment The fragment on which this feature will live.
* @param authenticationCallbacks Callbacks for BiometricPrompt.
*/
-
class BiometricPromptFeature(
private val context: Context,
private val fragment: Fragment,
@@ -78,37 +74,4 @@ class BiometricPromptFeature(
authenticationCallbacks.onAuthFailure.invoke()
}
}
-
- internal fun getAndroidBiometricManager(context: Context): BiometricManager {
- return BiometricManager.from(context)
- }
-
- /**
- * Checks if the appropriate SDK version and hardware capabilities are met to use the feature.
- */
- fun canUseFeature(context: Context): Boolean {
- return if (SDK_INT >= M) {
- val manager = getAndroidBiometricManager(context)
- isHardwareAvailable(manager) && isEnrolled(manager)
- } else {
- false
- }
- }
-
- /**
- * Checks if the hardware requirements are met for using the [BiometricManager].
- */
- fun isHardwareAvailable(biometricManager: BiometricManager): Boolean {
- val status = biometricManager.canAuthenticate(BIOMETRIC_WEAK)
- return status != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE &&
- status != BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
- }
-
- /**
- * Checks if the user can use the [BiometricManager] and is therefore enrolled.
- */
- fun isEnrolled(biometricManager: BiometricManager): Boolean {
- val status = biometricManager.canAuthenticate(BIOMETRIC_WEAK)
- return status == BiometricManager.BIOMETRIC_SUCCESS
- }
}
diff --git a/components/lib/biometric-prompt/src/main/java/mozilla.components.lib.auth/BiometricUtils.kt b/components/lib/biometric-prompt/src/main/java/mozilla.components.lib.auth/BiometricUtils.kt
new file mode 100644
index 00000000000..23092d76a88
--- /dev/null
+++ b/components/lib/biometric-prompt/src/main/java/mozilla.components.lib.auth/BiometricUtils.kt
@@ -0,0 +1,51 @@
+/* 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.lib.auth
+
+import android.content.Context
+import android.os.Build
+import androidx.annotation.VisibleForTesting
+import androidx.biometric.BiometricManager
+
+class BiometricUtils {
+
+ @VisibleForTesting
+ internal fun getAndroidBiometricManager(context: Context): BiometricManager {
+ return BiometricManager.from(context)
+ }
+
+ /**
+ * Checks if the appropriate SDK version and hardware capabilities are met to use the feature.
+ */
+ fun canUseFeature(context: Context): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ val manager = getAndroidBiometricManager(context)
+ isHardwareAvailable(manager) && isEnrolled(manager)
+ } else {
+ false
+ }
+ }
+
+ /**
+ * Checks if the hardware requirements are met for using the [BiometricManager].
+ */
+ @VisibleForTesting
+ internal fun isHardwareAvailable(biometricManager: BiometricManager): Boolean {
+ val status =
+ biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
+ return status != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE &&
+ status != BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
+ }
+
+ /**
+ * Checks if the user can use the [BiometricManager] and is therefore enrolled.
+ */
+ @VisibleForTesting
+ internal fun isEnrolled(biometricManager: BiometricManager): Boolean {
+ val status =
+ biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
+ return status == BiometricManager.BIOMETRIC_SUCCESS
+ }
+}
diff --git a/components/feature/biometric-prompt/src/test/java/mozilla/components/feature/biometric/BiometricPromptFeatureTest.kt b/components/lib/biometric-prompt/src/test/java/mozilla.components.lib.auth/BiometricPromptFeatureTest.kt
similarity index 74%
rename from components/feature/biometric-prompt/src/test/java/mozilla/components/feature/biometric/BiometricPromptFeatureTest.kt
rename to components/lib/biometric-prompt/src/test/java/mozilla.components.lib.auth/BiometricPromptFeatureTest.kt
index 5e1d9fcbfb7..90e669fe0d1 100644
--- a/components/feature/biometric-prompt/src/test/java/mozilla/components/feature/biometric/BiometricPromptFeatureTest.kt
+++ b/components/lib/biometric-prompt/src/test/java/mozilla.components.lib.auth/BiometricPromptFeatureTest.kt
@@ -2,7 +2,7 @@
* 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.feature.biometric
+package mozilla.components.lib.auth
import android.os.Build.VERSION_CODES.LOLLIPOP
import android.os.Build.VERSION_CODES.M
@@ -32,10 +32,12 @@ class BiometricPromptFeatureTest {
private lateinit var biometricPromptFeature: BiometricPromptFeature
private lateinit var biometricManager: BiometricManager
private lateinit var fragment: Fragment
+ private lateinit var biometricUtils: BiometricUtils
@Before
fun setup() {
fragment = createAddedTestFragment { Fragment() }
+ biometricUtils = spy(BiometricUtils())
biometricPromptFeature = spy(
BiometricPromptFeature(
testContext,
@@ -51,47 +53,45 @@ class BiometricPromptFeatureTest {
)
)
biometricManager = mock()
-
- doReturn(biometricManager).`when`(biometricPromptFeature)
- .getAndroidBiometricManager(testContext)
+ doReturn(biometricManager).`when`(biometricUtils).getAndroidBiometricManager(testContext)
}
@Config(sdk = [LOLLIPOP])
@Test
fun `canUseFeature checks for SDK compatible`() {
- assertFalse(biometricPromptFeature.canUseFeature(testContext))
+ assertFalse(biometricUtils.canUseFeature(testContext))
}
@Config(sdk = [M])
@Test
fun `GIVEN canUseFeature is called WHEN hardware is available and biometric is enrolled THEN canUseFeature return true`() {
- doReturn(true).`when`(biometricPromptFeature).isHardwareAvailable(biometricManager)
- doReturn(true).`when`(biometricPromptFeature).isEnrolled(biometricManager)
- assertTrue(biometricPromptFeature.canUseFeature(testContext))
+ doReturn(true).`when`(biometricUtils).isHardwareAvailable(biometricManager)
+ doReturn(true).`when`(biometricUtils).isEnrolled(biometricManager)
+ assertTrue(biometricUtils.canUseFeature(testContext))
}
@Config(sdk = [M])
@Test
fun `GIVEN canUseFeature is called WHEN hardware is available and biometric is not enrolled THEN canUseFeature return false`() {
- doReturn(false).`when`(biometricPromptFeature).isHardwareAvailable(biometricManager)
- doReturn(true).`when`(biometricPromptFeature).isEnrolled(biometricManager)
- assertFalse(biometricPromptFeature.canUseFeature(testContext))
+ doReturn(false).`when`(biometricUtils).isHardwareAvailable(biometricManager)
+ doReturn(true).`when`(biometricUtils).isEnrolled(biometricManager)
+ assertFalse(biometricUtils.canUseFeature(testContext))
}
@Config(sdk = [M])
@Test
fun `GIVEN canUseFeature is called WHEN hardware is not available and biometric is not enrolled THEN canUseFeature return false`() {
- doReturn(false).`when`(biometricPromptFeature).isHardwareAvailable(biometricManager)
- doReturn(false).`when`(biometricPromptFeature).isEnrolled(biometricManager)
- assertFalse(biometricPromptFeature.canUseFeature(testContext))
+ doReturn(false).`when`(biometricUtils).isHardwareAvailable(biometricManager)
+ doReturn(false).`when`(biometricUtils).isEnrolled(biometricManager)
+ assertFalse(biometricUtils.canUseFeature(testContext))
}
@Config(sdk = [M])
@Test
fun `GIVEN canUseFeature is called WHEN hardware is not available and biometric is enrolled THEN canUseFeature return false`() {
- doReturn(false).`when`(biometricPromptFeature).isHardwareAvailable(biometricManager)
- doReturn(true).`when`(biometricPromptFeature).isEnrolled(biometricManager)
- assertFalse(biometricPromptFeature.canUseFeature(testContext))
+ doReturn(false).`when`(biometricUtils).isHardwareAvailable(biometricManager)
+ doReturn(true).`when`(biometricUtils).isEnrolled(biometricManager)
+ assertFalse(biometricUtils.canUseFeature(testContext))
}
@Test
@@ -124,9 +124,9 @@ class BiometricPromptFeatureTest {
val prompt = BiometricPrompt(fragment, promptCallback)
biometricPromptFeature.biometricPrompt = prompt
- promptCallback.onAuthenticationError(0, "")
+ promptCallback.onAuthenticationError(BiometricPrompt.ERROR_CANCELED, "")
- verify(promptCallback).onAuthenticationError(0, "")
+ verify(promptCallback).onAuthenticationError(BiometricPrompt.ERROR_CANCELED, "")
promptCallback.onAuthenticationFailed()
diff --git a/components/lib/biometric-prompt/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/components/lib/biometric-prompt/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 00000000000..cf1c399ea81
--- /dev/null
+++ b/components/lib/biometric-prompt/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)
diff --git a/components/browser/session-storage/src/test/resources/robolectric.properties b/components/lib/biometric-prompt/src/test/resources/robolectric.properties
similarity index 100%
rename from components/browser/session-storage/src/test/resources/robolectric.properties
rename to components/lib/biometric-prompt/src/test/resources/robolectric.properties
diff --git a/components/lib/crash/src/main/res/values-ast/strings.xml b/components/lib/crash/src/main/res/values-ast/strings.xml
index db77b03b0df..59e83337367 100644
--- a/components/lib/crash/src/main/res/values-ast/strings.xml
+++ b/components/lib/crash/src/main/res/values-ast/strings.xml
@@ -1,7 +1,7 @@
- Perdona mas %1$s tuvo un problema y cascó.
+ Sentímoslo, %1$s tuvo un problema y cascó.Unviar l\'informe del fallu a %1$s
@@ -15,6 +15,9 @@
Casques
+
+ Sentímoslo, asocedió un problema en %1$s.
+
Informar
diff --git a/components/lib/crash/src/main/res/values-ban/strings.xml b/components/lib/crash/src/main/res/values-ban/strings.xml
index b21c5968d2f..f60b516a088 100644
--- a/components/lib/crash/src/main/res/values-ban/strings.xml
+++ b/components/lib/crash/src/main/res/values-ban/strings.xml
@@ -1,6 +1,11 @@
- Ampura. %1$s gelah galat lan usak.
+ Ampura. %1$s wenten galat lan usak.
-
+
+ Gatra
+
+
+ Bagiang
+
diff --git a/components/lib/crash/src/main/res/values-eo/strings.xml b/components/lib/crash/src/main/res/values-eo/strings.xml
index fd914bdd176..068ead76d24 100644
--- a/components/lib/crash/src/main/res/values-eo/strings.xml
+++ b/components/lib/crash/src/main/res/values-eo/strings.xml
@@ -16,6 +16,9 @@
Paneoj
+
+ Bedaŭrinde problemo okazis en %1$s.
+
Raporto
diff --git a/components/lib/crash/src/main/res/values-kmr/strings.xml b/components/lib/crash/src/main/res/values-kmr/strings.xml
index 2c53067d9bb..7dfe7c4c99b 100644
--- a/components/lib/crash/src/main/res/values-kmr/strings.xml
+++ b/components/lib/crash/src/main/res/values-kmr/strings.xml
@@ -16,6 +16,9 @@
Têkçûn
+
+ Bibore. Di %1$s`ê de problemek derket.
+
Rapor bike
diff --git a/components/lib/crash/src/main/res/values-skr/strings.xml b/components/lib/crash/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..64f84764a5a
--- /dev/null
+++ b/components/lib/crash/src/main/res/values-skr/strings.xml
@@ -0,0 +1,36 @@
+
+
+
+
+ افسوس۔ %1$s وچ کوئی مسئلہ ہے تے تباہ تھی ڳئے۔
+
+
+ %1$s کوں کریش رپوٹ بھیڄو
+
+
+ بند کرو
+
+
+ %1$s ولدا شروع کرو
+
+
+ کریش
+
+
+ افسوس۔ %1$s وچ ہک مسئلہ تھی ڳیا ہے۔
+
+
+ رپورٹ کرو
+
+
+ %1$s کوں کریش رپوٹ بھیڄیندا پئے
+
+
+ کریش رپورٹاں
+
+
+ کوئی کریش رپوٹاں جمع کائنی کرائیاں۔
+
+
+ شیئر
+
diff --git a/components/lib/crash/src/main/res/values-tg/strings.xml b/components/lib/crash/src/main/res/values-tg/strings.xml
index a7a8b5d93b2..c6d4814d9fa 100644
--- a/components/lib/crash/src/main/res/values-tg/strings.xml
+++ b/components/lib/crash/src/main/res/values-tg/strings.xml
@@ -1,10 +1,10 @@
- Мутаассифона, %1$s мушкилӣ дошта, ба садама дучор шуд.
+ Мутаассифона, %1$s мушкилӣ дошта, бо вайронӣ дучор шуд.
- Фиристодани гузориш дар бораи садама ба %1$s
+ Фиристодани гузориш дар бораи вайронӣ ба %1$sПӯшидан
@@ -13,7 +13,7 @@
Аз нав оғоз кардани %1$s
- Садамот
+ ВайрониҳоБубахшед. Дар %1$s мушкилӣ ба миён омад.
@@ -22,13 +22,13 @@
Гузориш додан
- Гузориш дар бораи садама ба %1$s фиристода шуда истодааст
+ Гузориш дар бораи вайронӣ ба %1$s фиристода шуда истодааст
- Гузоришҳо дар бораи садама
+ Гузоришҳо дар бораи вайронӣ
- Ягон гузориш дар бораи садама пешниҳод карда нашуд.
+ Ягон гузориш дар бораи вайронӣ пешниҳод карда нашуд.Мубодила кардан
diff --git a/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt b/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt
index 86ab0c04244..d348e8547dd 100644
--- a/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt
+++ b/components/lib/crash/src/test/java/mozilla/components/lib/crash/CrashReporterTest.kt
@@ -9,7 +9,6 @@ import android.app.PendingIntent
import android.content.Intent
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.lib.crash.service.CrashReporterService
import mozilla.components.lib.crash.service.CrashTelemetryService
@@ -43,7 +42,7 @@ class CrashReporterTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val scope = TestCoroutineScope(coroutinesTestRule.testDispatcher)
+ private val scope = coroutinesTestRule.scope
@Before
fun setUp() {
diff --git a/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/CrashHandlerServiceTest.kt b/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/CrashHandlerServiceTest.kt
index 21756ce858d..1005503cf70 100644
--- a/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/CrashHandlerServiceTest.kt
+++ b/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/CrashHandlerServiceTest.kt
@@ -7,15 +7,13 @@ package mozilla.components.lib.crash.handler
import android.content.ComponentName
import android.content.Intent
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.lib.crash.CrashReporter
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -36,7 +34,7 @@ class CrashHandlerServiceTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val scope: CoroutineScope = TestCoroutineScope(coroutinesTestRule.testDispatcher)
+ private val scope = coroutinesTestRule.scope
@Before
fun setUp() {
@@ -79,33 +77,33 @@ class CrashHandlerServiceTest {
}
@Test
- fun `CrashHandlerService forwards main process native code crash to crash reporter`() = runBlocking {
+ fun `CrashHandlerService forwards main process native code crash to crash reporter`() = runTestOnMain {
doNothing().`when`(reporter)!!.sendCrashReport(any(), any())
intent.putExtra("processType", "MAIN")
- service!!.handleCrashIntent(intent, scope)
+ service!!.handleCrashIntent(intent, coroutinesTestRule.scope)
verify(reporter)!!.onCrash(any(), any())
verify(reporter)!!.sendCrashReport(any(), any())
verify(reporter, never())!!.sendNonFatalCrashIntent(any(), any())
}
@Test
- fun `CrashHandlerService forwards foreground child process native code crash to crash reporter`() = runBlocking {
+ fun `CrashHandlerService forwards foreground child process native code crash to crash reporter`() = runTestOnMain {
doNothing().`when`(reporter)!!.sendCrashReport(any(), any())
intent.putExtra("processType", "FOREGROUND_CHILD")
- service!!.handleCrashIntent(intent, scope)
+ service!!.handleCrashIntent(intent, coroutinesTestRule.scope)
verify(reporter)!!.onCrash(any(), any())
verify(reporter)!!.sendNonFatalCrashIntent(any(), any())
verify(reporter, never())!!.sendCrashReport(any(), any())
}
@Test
- fun `CrashHandlerService forwards background child process native code crash to crash reporter`() = runBlocking {
+ fun `CrashHandlerService forwards background child process native code crash to crash reporter`() = runTestOnMain {
doNothing().`when`(reporter)!!.sendCrashReport(any(), any())
intent.putExtra("processType", "BACKGROUND_CHILD")
- service!!.handleCrashIntent(intent, scope)
+ service!!.handleCrashIntent(intent, coroutinesTestRule.scope)
verify(reporter)!!.onCrash(any(), any())
verify(reporter)!!.sendCrashReport(any(), any())
verify(reporter, never())!!.sendNonFatalCrashIntent(any(), any())
diff --git a/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/ExceptionHandlerTest.kt b/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/ExceptionHandlerTest.kt
index 34a34418fab..d30f919d81a 100644
--- a/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/ExceptionHandlerTest.kt
+++ b/components/lib/crash/src/test/java/mozilla/components/lib/crash/handler/ExceptionHandlerTest.kt
@@ -6,7 +6,6 @@ package mozilla.components.lib.crash.handler
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.lib.crash.Crash
import mozilla.components.lib.crash.CrashReporter
@@ -29,7 +28,7 @@ class ExceptionHandlerTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val scope = TestCoroutineScope(coroutinesTestRule.testDispatcher)
+ private val scope = coroutinesTestRule.scope
@Test
fun `ExceptionHandler forwards crashes to CrashReporter`() {
diff --git a/components/lib/crash/src/test/java/mozilla/components/lib/crash/prompt/CrashReporterActivityTest.kt b/components/lib/crash/src/test/java/mozilla/components/lib/crash/prompt/CrashReporterActivityTest.kt
index 2d0f3899c8c..f5e5a120ae6 100644
--- a/components/lib/crash/src/test/java/mozilla/components/lib/crash/prompt/CrashReporterActivityTest.kt
+++ b/components/lib/crash/src/test/java/mozilla/components/lib/crash/prompt/CrashReporterActivityTest.kt
@@ -15,8 +15,7 @@ import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ActivityScenario.launch
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.advanceUntilIdle
import mozilla.components.lib.crash.Crash
import mozilla.components.lib.crash.CrashReporter
import mozilla.components.lib.crash.prompt.CrashReporterActivity.Companion.PREFERENCE_KEY_SEND_REPORT
@@ -25,6 +24,7 @@ import mozilla.components.lib.crash.service.CrashReporterService
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -35,6 +35,7 @@ import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations.openMocks
+import kotlin.coroutines.CoroutineContext
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@@ -42,7 +43,7 @@ class CrashReporterActivityTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val scope = TestCoroutineScope(coroutinesTestRule.testDispatcher)
+ private val scope = coroutinesTestRule.scope
@Mock
lateinit var service: CrashReporterService
@@ -53,7 +54,7 @@ class CrashReporterActivityTest {
}
@Test
- fun `Pressing close button sends report`() = runBlockingTest {
+ fun `Pressing close button sends report`() = runTestOnMain {
CrashReporter(
context = testContext,
shouldPrompt = CrashReporter.Prompt.ALWAYS,
@@ -62,7 +63,7 @@ class CrashReporterActivityTest {
).install(testContext)
val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Hello World"), arrayListOf())
- val scenario = launchActivityWith(crash)
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
scenario.onActivity { activity ->
// When
@@ -77,7 +78,7 @@ class CrashReporterActivityTest {
}
@Test
- fun `Pressing restart button sends report`() = runBlockingTest {
+ fun `Pressing restart button sends report`() = runTestOnMain {
CrashReporter(
context = testContext,
shouldPrompt = CrashReporter.Prompt.ALWAYS,
@@ -86,7 +87,7 @@ class CrashReporterActivityTest {
).install(testContext)
val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Hello World"), arrayListOf())
- val scenario = launchActivityWith(crash)
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
scenario.onActivity { activity ->
// When
@@ -101,7 +102,7 @@ class CrashReporterActivityTest {
}
@Test
- fun `Custom message is set on CrashReporterActivity`() = runBlockingTest {
+ fun `Custom message is set on CrashReporterActivity`() = runTestOnMain {
CrashReporter(
context = testContext,
shouldPrompt = CrashReporter.Prompt.ALWAYS,
@@ -113,7 +114,7 @@ class CrashReporterActivityTest {
).install(testContext)
val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Hello World"), arrayListOf())
- val scenario = launchActivityWith(crash)
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
scenario.onActivity { activity ->
// Then
@@ -122,7 +123,7 @@ class CrashReporterActivityTest {
}
@Test
- fun `Sending crash report saves checkbox state`() = runBlockingTest {
+ fun `Sending crash report saves checkbox state`() = runTestOnMain {
CrashReporter(
context = testContext,
shouldPrompt = CrashReporter.Prompt.ALWAYS,
@@ -131,7 +132,7 @@ class CrashReporterActivityTest {
).install(testContext)
val crash = Crash.UncaughtExceptionCrash(0, RuntimeException("Hello World"), arrayListOf())
- val scenario = launchActivityWith(crash)
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
scenario.onActivity { activity ->
// When
@@ -149,7 +150,7 @@ class CrashReporterActivityTest {
}
@Test
- fun `Restart button visible for main process crash`() = runBlockingTest {
+ fun `Restart button visible for main process crash`() = runTestOnMain {
CrashReporter(
context = testContext,
shouldPrompt = CrashReporter.Prompt.ALWAYS,
@@ -165,7 +166,7 @@ class CrashReporterActivityTest {
Crash.NativeCodeCrash.PROCESS_TYPE_MAIN,
arrayListOf()
)
- val scenario = launchActivityWith(crash)
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
scenario.onActivity { activity ->
assertEquals(activity.restartButton.visibility, View.VISIBLE)
@@ -173,7 +174,7 @@ class CrashReporterActivityTest {
}
@Test
- fun `Restart button hidden for background child process crash`() = runBlockingTest {
+ fun `Restart button hidden for background child process crash`() = runTestOnMain {
CrashReporter(
context = testContext,
shouldPrompt = CrashReporter.Prompt.ALWAYS,
@@ -189,7 +190,7 @@ class CrashReporterActivityTest {
Crash.NativeCodeCrash.PROCESS_TYPE_BACKGROUND_CHILD,
arrayListOf()
)
- val scenario = launchActivityWith(crash)
+ val scenario = coroutineContext.launchActivityWithCrash(crash)
scenario.onActivity { activity ->
assertEquals(activity.restartButton.visibility, View.GONE)
@@ -201,7 +202,7 @@ class CrashReporterActivityTest {
* Launch activity scenario for certain [crash].
*/
@ExperimentalCoroutinesApi
-private fun TestCoroutineScope.launchActivityWith(
+private fun CoroutineContext.launchActivityWithCrash(
crash: Crash
): ActivityScenario = run {
val intent = Intent(testContext, CrashReporterActivity::class.java)
@@ -209,7 +210,7 @@ private fun TestCoroutineScope.launchActivityWith(
launch(intent).apply {
onActivity { activity ->
- activity.reporterCoroutineContext = coroutineContext
+ activity.reporterCoroutineContext = this@run
}
}
}
diff --git a/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt b/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt
index 588333fe9bd..81b1317337b 100644
--- a/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt
+++ b/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashReportServiceTest.kt
@@ -8,7 +8,6 @@ import android.content.ComponentName
import android.content.Intent
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.lib.crash.Crash
import mozilla.components.lib.crash.CrashReporter
@@ -37,7 +36,7 @@ class SendCrashReportServiceTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val scope = TestCoroutineScope(coroutinesTestRule.testDispatcher)
+ private val scope = coroutinesTestRule.scope
@Before
fun setUp() {
diff --git a/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashTelemetryServiceTest.kt b/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashTelemetryServiceTest.kt
index ff6f68f3228..7e1a6ce15de 100644
--- a/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashTelemetryServiceTest.kt
+++ b/components/lib/crash/src/test/java/mozilla/components/lib/crash/service/SendCrashTelemetryServiceTest.kt
@@ -8,7 +8,6 @@ import android.content.ComponentName
import android.content.Intent
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.lib.crash.Crash
import mozilla.components.lib.crash.CrashReporter
import mozilla.components.support.test.any
@@ -35,7 +34,7 @@ class SendCrashTelemetryServiceTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val scope = TestCoroutineScope(coroutinesTestRule.testDispatcher)
+ private val scope = coroutinesTestRule.scope
@Before
fun setUp() {
diff --git a/components/lib/publicsuffixlist/build.gradle b/components/lib/publicsuffixlist/build.gradle
index fb42f52063b..26fcff09be6 100644
--- a/components/lib/publicsuffixlist/build.gradle
+++ b/components/lib/publicsuffixlist/build.gradle
@@ -39,6 +39,7 @@ dependencies {
testImplementation Dependencies.androidx_test_junit
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
+ testImplementation Dependencies.testing_coroutines
}
apply from: '../../../publish.gradle'
diff --git a/components/lib/publicsuffixlist/src/test/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListTest.kt b/components/lib/publicsuffixlist/src/test/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListTest.kt
index dcb624551a8..9526509aa70 100644
--- a/components/lib/publicsuffixlist/src/test/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListTest.kt
+++ b/components/lib/publicsuffixlist/src/test/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListTest.kt
@@ -5,7 +5,8 @@
package mozilla.components.lib.publicsuffixlist
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -14,6 +15,7 @@ import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class PublicSuffixListTest {
@@ -21,7 +23,7 @@ class PublicSuffixListTest {
get() = PublicSuffixList(testContext)
@Test
- fun `Verify getPublicSuffixPlusOne for known domains`() = runBlocking {
+ fun `Verify getPublicSuffixPlusOne for known domains`() = runTest {
assertEquals(
"mozilla.org",
publicSuffixList.getPublicSuffixPlusOne("www.mozilla.org").await()
@@ -69,7 +71,7 @@ class PublicSuffixListTest {
}
@Test
- fun `Verify getPublicSuffix for known domains`() = runBlocking {
+ fun `Verify getPublicSuffix for known domains`() = runTest {
assertEquals(
"org",
publicSuffixList.getPublicSuffix("www.mozilla.org").await()
@@ -117,7 +119,7 @@ class PublicSuffixListTest {
}
@Test
- fun `Verify stripPublicSuffix for known domains`() = runBlocking {
+ fun `Verify stripPublicSuffix for known domains`() = runTest {
assertEquals(
"www.mozilla",
publicSuffixList.stripPublicSuffix("www.mozilla.org").await()
@@ -169,7 +171,7 @@ class PublicSuffixListTest {
* https://raw.githubusercontent.com/publicsuffix/list/master/tests/test_psl.txt
*/
@Test
- fun `Verify getPublicSuffixPlusOne against official test data`() = runBlocking {
+ fun `Verify getPublicSuffixPlusOne against official test data`() = runTest {
// empty input
assertNull(publicSuffixList.getPublicSuffixPlusOne("").await())
@@ -410,7 +412,7 @@ class PublicSuffixListTest {
}
@Test
- fun `Accessing with and without prefetch`() = runBlocking {
+ fun `Accessing with and without prefetch`() = runTest {
run {
val publicSuffixList = PublicSuffixList(testContext)
assertEquals("org", publicSuffixList.getPublicSuffix("mozilla.org").await())
@@ -425,7 +427,7 @@ class PublicSuffixListTest {
}
@Test
- fun `Verify isPublicSuffix with known and unknown suffixes`() = runBlocking {
+ fun `Verify isPublicSuffix with known and unknown suffixes`() = runTest {
assertTrue(publicSuffixList.isPublicSuffix("org").await())
assertTrue(publicSuffixList.isPublicSuffix("com").await())
assertTrue(publicSuffixList.isPublicSuffix("us").await())
@@ -454,7 +456,7 @@ class PublicSuffixListTest {
* https://github.com/google/guava/blob/master/guava-tests/test/com/google/common/net/InternetDomainNameTest.java
*/
@Test
- fun `Verify getPublicSuffix can handle obscure and invalid input`() = runBlocking {
+ fun `Verify getPublicSuffix can handle obscure and invalid input`() = runTest {
assertEquals("cOM", publicSuffixList.getPublicSuffix("f-_-o.cOM").await())
assertEquals("com", publicSuffixList.getPublicSuffix("f11-1.com").await())
assertNull(publicSuffixList.getPublicSuffix("www").await())
diff --git a/components/lib/state/src/test/java/mozilla/components/lib/state/ext/FragmentKtTest.kt b/components/lib/state/src/test/java/mozilla/components/lib/state/ext/FragmentKtTest.kt
index 37907b95b82..1c677bf33c7 100644
--- a/components/lib/state/src/test/java/mozilla/components/lib/state/ext/FragmentKtTest.kt
+++ b/components/lib/state/src/test/java/mozilla/components/lib/state/ext/FragmentKtTest.kt
@@ -13,7 +13,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.test.setMain
import mozilla.components.lib.state.Store
import mozilla.components.lib.state.TestAction
diff --git a/components/lib/state/src/test/java/mozilla/components/lib/state/ext/StoreExtensionsKtTest.kt b/components/lib/state/src/test/java/mozilla/components/lib/state/ext/StoreExtensionsKtTest.kt
index 0da2ef1e251..9551d8b7034 100644
--- a/components/lib/state/src/test/java/mozilla/components/lib/state/ext/StoreExtensionsKtTest.kt
+++ b/components/lib/state/src/test/java/mozilla/components/lib/state/ext/StoreExtensionsKtTest.kt
@@ -19,10 +19,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.channels.consumeEach
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.lib.state.Store
import mozilla.components.lib.state.TestAction
import mozilla.components.lib.state.TestState
@@ -30,6 +27,7 @@ import mozilla.components.lib.state.reducer
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -48,10 +46,9 @@ class StoreExtensionsKtTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
- private val testDispatcher = coroutinesTestRule.testDispatcher
@Test
- fun `Observer will not get registered if lifecycle is already destroyed`() {
+ fun `Observer will not get registered if lifecycle is already destroyed`() = runTestOnMain {
val owner = MockedLifecycleOwner(Lifecycle.State.DESTROYED)
val store = Store(
@@ -141,7 +138,7 @@ class StoreExtensionsKtTest {
@Test
@Synchronized
@ExperimentalCoroutinesApi // Channel
- fun `Reading state updates from channel`() {
+ fun `Reading state updates from channel`() = runTestOnMain {
val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
val store = Store(
@@ -186,7 +183,7 @@ class StoreExtensionsKtTest {
assertEquals(26, receivedValue)
latch = CountDownLatch(1)
- runBlocking { job.cancelAndJoin() }
+ job.cancelAndJoin()
assertTrue(channel.isClosedForReceive)
store.dispatch(TestAction.IncrementAction).joinBlocking()
@@ -210,7 +207,7 @@ class StoreExtensionsKtTest {
@Test
@Synchronized
@ExperimentalCoroutinesApi
- fun `Reading state updates from Flow with lifecycle owner`() {
+ fun `Reading state updates from Flow with lifecycle owner`() = runTestOnMain {
val owner = MockedLifecycleOwner(Lifecycle.State.INITIALIZED)
val store = Store(
@@ -223,7 +220,7 @@ class StoreExtensionsKtTest {
val flow = store.flow(owner)
- val job = TestCoroutineScope(testDispatcher).launch {
+ val job = coroutinesTestRule.scope.launch {
flow.collect { state ->
receivedValue = state.counter
latch.countDown()
@@ -256,7 +253,7 @@ class StoreExtensionsKtTest {
assertEquals(26, receivedValue)
latch = CountDownLatch(1)
- runBlocking { job.cancelAndJoin() }
+ job.cancelAndJoin()
// Receiving nothing anymore since coroutine is cancelled
store.dispatch(TestAction.IncrementAction).joinBlocking()
@@ -315,7 +312,7 @@ class StoreExtensionsKtTest {
@Test
@Synchronized
@ExperimentalCoroutinesApi
- fun `Reading state updates from Flow without lifecycle owner`() {
+ fun `Reading state updates from Flow without lifecycle owner`() = runTestOnMain {
val store = Store(
TestState(counter = 23),
::reducer
@@ -354,7 +351,7 @@ class StoreExtensionsKtTest {
latch = CountDownLatch(1)
- runBlocking { job.cancelAndJoin() }
+ job.cancelAndJoin()
// Receiving nothing anymore since coroutine is cancelled
store.dispatch(TestAction.IncrementAction).joinBlocking()
@@ -515,7 +512,7 @@ class StoreExtensionsKtTest {
}
@Test
- fun `Observer bound to view will not get notified about state changes until the view is attached`() {
+ fun `Observer bound to view will not get notified about state changes until the view is attached`() = runTestOnMain {
val activity = Robolectric.buildActivity(Activity::class.java).create().get()
val view = View(testContext)
diff --git a/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt b/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt
index 38403a80d59..3bacd920d51 100644
--- a/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt
+++ b/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt
@@ -6,7 +6,6 @@ package mozilla.components.lib.state.helpers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collect
import mozilla.components.lib.state.Store
import mozilla.components.lib.state.TestAction
import mozilla.components.lib.state.TestState
diff --git a/components/service/contile/build.gradle b/components/service/contile/build.gradle
index 7c8d3c4af74..22329cd4edf 100644
--- a/components/service/contile/build.gradle
+++ b/components/service/contile/build.gradle
@@ -36,6 +36,7 @@ dependencies {
testImplementation Dependencies.androidx_work_testing
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
+ testImplementation Dependencies.testing_coroutines
testImplementation project(':support-test')
}
diff --git a/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesProviderTest.kt b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesProviderTest.kt
index 620e574c554..23ab7c62bb0 100644
--- a/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesProviderTest.kt
+++ b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesProviderTest.kt
@@ -5,7 +5,8 @@
package mozilla.components.service.contile
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.Response
import mozilla.components.support.test.any
@@ -27,11 +28,12 @@ import java.io.File
import java.io.IOException
import java.util.Date
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class ContileTopSitesProviderTest {
@Test
- fun `GIVEN a successful status response WHEN top sites are fetched THEN response should contain top sites`() = runBlocking {
+ fun `GIVEN a successful status response WHEN top sites are fetched THEN response should contain top sites`() = runTest {
val client = prepareClient()
val provider = ContileTopSitesProvider(testContext, client)
val topSites = provider.getTopSites()
@@ -56,7 +58,7 @@ class ContileTopSitesProviderTest {
}
@Test(expected = IOException::class)
- fun `GIVEN a 500 status response WHEN top sites are fetched THEN throw an exception`() = runBlocking {
+ fun `GIVEN a 500 status response WHEN top sites are fetched THEN throw an exception`() = runTest {
val client = prepareClient(status = 500)
val provider = ContileTopSitesProvider(testContext, client)
provider.getTopSites()
@@ -64,7 +66,7 @@ class ContileTopSitesProviderTest {
}
@Test
- fun `GIVEN a cache configuration is allowed and not expired WHEN top sites are fetched THEN read from the disk cache`() = runBlocking {
+ fun `GIVEN a cache configuration is allowed and not expired WHEN top sites are fetched THEN read from the disk cache`() = runTest {
val client = prepareClient()
val provider = spy(ContileTopSitesProvider(testContext, client))
@@ -83,7 +85,7 @@ class ContileTopSitesProviderTest {
}
@Test
- fun `GIVEN a cache configuration is allowed WHEN top sites are fetched THEN write response to cache`() = runBlocking {
+ fun `GIVEN a cache configuration is allowed WHEN top sites are fetched THEN write response to cache`() = runTest {
val jsonResponse = loadResourceAsString("/contile/contile.json")
val client = prepareClient(jsonResponse)
val provider = spy(ContileTopSitesProvider(testContext, client))
@@ -175,7 +177,7 @@ class ContileTopSitesProviderTest {
}
@Test
- fun `GIVEN cache is not expired WHEN top sites are refreshed THEN do nothing`() = runBlocking {
+ fun `GIVEN cache is not expired WHEN top sites are refreshed THEN do nothing`() = runTest {
val provider = spy(
ContileTopSitesProvider(
testContext,
@@ -192,7 +194,7 @@ class ContileTopSitesProviderTest {
}
@Test
- fun `GIVEN cache is expired WHEN top sites are refreshed THEN fetch and write new response to cache`() = runBlocking {
+ fun `GIVEN cache is expired WHEN top sites are refreshed THEN fetch and write new response to cache`() = runTest {
val jsonResponse = loadResourceAsString("/contile/contile.json")
val provider = spy(
ContileTopSitesProvider(
diff --git a/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterTest.kt b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterTest.kt
index de3fb5c745b..799ad712498 100644
--- a/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterTest.kt
+++ b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterTest.kt
@@ -10,7 +10,8 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.await
import androidx.work.testing.WorkManagerTestInitHelper
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.service.contile.ContileTopSitesUpdater.Companion.PERIODIC_WORK_TAG
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
@@ -23,6 +24,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class ContileTopSitesUpdaterTest {
@@ -40,7 +42,7 @@ class ContileTopSitesUpdaterTest {
}
@Test
- fun `WHEN periodic work is started THEN work is queued`() = runBlocking {
+ fun `WHEN periodic work is started THEN work is queued`() = runTest {
val updater = ContileTopSitesUpdater(testContext, provider = mock())
val workManager = WorkManager.getInstance(testContext)
var workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await()
@@ -62,7 +64,7 @@ class ContileTopSitesUpdaterTest {
}
@Test
- fun `GIVEN periodic work is started WHEN period work is stopped THEN no work is queued`() = runBlocking {
+ fun `GIVEN periodic work is started WHEN period work is stopped THEN no work is queued`() = runTest {
val updater = ContileTopSitesUpdater(testContext, provider = mock())
val workManager = WorkManager.getInstance(testContext)
var workInfo = workManager.getWorkInfosForUniqueWork(PERIODIC_WORK_TAG).await()
diff --git a/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorkerTest.kt b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorkerTest.kt
index 9757ae50ddd..e9cafd31e81 100644
--- a/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorkerTest.kt
+++ b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUpdaterWorkerTest.kt
@@ -8,7 +8,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.work.ListenableWorker
import androidx.work.await
import androidx.work.testing.TestListenableWorkerBuilder
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.whenever
@@ -20,6 +21,7 @@ import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.spy
import java.io.IOException
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class ContileTopSitesUpdaterWorkerTest {
@@ -29,7 +31,7 @@ class ContileTopSitesUpdaterWorkerTest {
}
@Test
- fun `WHEN worker does successful work THEN return a success result`() = runBlocking {
+ fun `WHEN worker does successful work THEN return a success result`() = runTest {
val provider: ContileTopSitesProvider = mock()
val worker = spy(
TestListenableWorkerBuilder(testContext)
@@ -46,7 +48,7 @@ class ContileTopSitesUpdaterWorkerTest {
}
@Test
- fun `WHEN worker does unsuccessful work THEN return a failure result`() = runBlocking {
+ fun `WHEN worker does unsuccessful work THEN return a failure result`() = runTest {
val provider: ContileTopSitesProvider = mock()
val worker = spy(
TestListenableWorkerBuilder(testContext)
diff --git a/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUseCasesTest.kt b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUseCasesTest.kt
index bab461359e4..306cd50de74 100644
--- a/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUseCasesTest.kt
+++ b/components/service/contile/src/test/java/mozilla/components/service/contile/ContileTopSitesUseCasesTest.kt
@@ -5,7 +5,8 @@
package mozilla.components.service.contile
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.support.test.eq
import mozilla.components.support.test.mock
import mozilla.components.support.test.whenever
@@ -15,11 +16,12 @@ import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.Mockito.verify
import java.io.IOException
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class ContileTopSitesUseCasesTest {
@Test
- fun `WHEN refresh contile top site use case is called THEN call the provider to fetch top sites bypassing the cache`() = runBlocking {
+ fun `WHEN refresh contile top site use case is called THEN call the provider to fetch top sites bypassing the cache`() = runTest {
val provider: ContileTopSitesProvider = mock()
ContileTopSitesUseCases.initialize(provider)
@@ -34,7 +36,7 @@ class ContileTopSitesUseCasesTest {
}
@Test(expected = IOException::class)
- fun `GIVEN the provider fails to fetch contile top sites WHEN refresh contile top site use case is called THEN an exception is thrown`() = runBlocking {
+ fun `GIVEN the provider fails to fetch contile top sites WHEN refresh contile top site use case is called THEN an exception is thrown`() = runTest {
val provider: ContileTopSitesProvider = mock()
val throwable = IOException("test")
diff --git a/components/service/firefox-accounts/build.gradle b/components/service/firefox-accounts/build.gradle
index 48401c6f517..ecef8c139c2 100644
--- a/components/service/firefox-accounts/build.gradle
+++ b/components/service/firefox-accounts/build.gradle
@@ -41,6 +41,7 @@ dependencies {
implementation project(':support-sync-telemetry')
implementation project(':support-ktx')
implementation project(':lib-dataprotect')
+ implementation project(':lib-state')
implementation Dependencies.kotlin_stdlib
implementation Dependencies.kotlin_coroutines
@@ -48,6 +49,7 @@ dependencies {
implementation Dependencies.androidx_work_runtime
testImplementation project(':support-test')
+ testImplementation project(':support-test-libstate')
testImplementation Dependencies.androidx_test_core
testImplementation Dependencies.androidx_test_junit
testImplementation Dependencies.androidx_work_testing
diff --git a/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt b/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt
new file mode 100644
index 00000000000..6827f08e8e4
--- /dev/null
+++ b/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt
@@ -0,0 +1,28 @@
+/* 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.service.fxa.store
+
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.lib.state.Action
+
+/**
+ * Actions for updating the global [SyncState] via [SyncStore].
+ */
+sealed class SyncAction : Action {
+ /**
+ * Update the [SyncState.status] of the [SyncStore].
+ */
+ data class UpdateSyncStatus(val status: SyncStatus) : SyncAction()
+
+ /**
+ * Update the [SyncState.account] of the [SyncStore].
+ */
+ data class UpdateAccount(val account: Account) : SyncAction()
+
+ /**
+ * Update the [SyncState.constellationState] of the [SyncStore].
+ */
+ data class UpdateDeviceConstellation(val deviceConstellation: ConstellationState) : SyncAction()
+}
diff --git a/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt b/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt
new file mode 100644
index 00000000000..d93ef47a1a8
--- /dev/null
+++ b/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt
@@ -0,0 +1,61 @@
+/* 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.service.fxa.store
+
+import mozilla.components.concept.sync.Avatar
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.Profile
+import mozilla.components.lib.state.State
+import mozilla.components.service.fxa.sync.WorkManagerSyncManager
+
+/**
+ * Global state of Sync.
+ *
+ * @property status The current status of Sync.
+ * @property account The current Sync account, if any.
+ * @property constellationState The current constellation state, if any.
+ */
+data class SyncState(
+ val status: SyncStatus = SyncStatus.NotInitialized,
+ val account: Account? = null,
+ val constellationState: ConstellationState? = null,
+) : State
+
+/**
+ * Various statuses described the [SyncState].
+ *
+ * Starts as [NotInitialized].
+ * Becomes [Started] during the length of a Sync.
+ * Becomes [Idle] when a Sync is completed.
+ * Becomes [Error] when a Sync encounters an error.
+ *
+ * See [WorkManagerSyncManager] for implementation details.
+ */
+enum class SyncStatus {
+ Started,
+ Idle,
+ Error,
+ NotInitialized
+}
+
+/**
+ * Account information available for a synced account.
+ *
+ * @property uid See [Profile.uid].
+ * @property email See [Profile.email].
+ * @property avatar See [Profile.avatar].
+ * @property displayName See [Profile.displayName].
+ * @property currentDeviceId See [OAuthAccount.getCurrentDeviceId].
+ * @property sessionToken See [OAuthAccount.getSessionToken].
+ */
+data class Account(
+ val uid: String?,
+ val email: String?,
+ val avatar: Avatar?,
+ val displayName: String?,
+ val currentDeviceId: String?,
+ val sessionToken: String?,
+)
diff --git a/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt b/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt
new file mode 100644
index 00000000000..ca688e0ce04
--- /dev/null
+++ b/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt
@@ -0,0 +1,28 @@
+/* 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.service.fxa.store
+
+import mozilla.components.lib.state.Middleware
+import mozilla.components.lib.state.Store
+
+/**
+ * [Store] for the global [SyncState]. This should likely be a singleton.
+ */
+class SyncStore(
+ middleware: List> = emptyList()
+) : Store(
+ initialState = SyncState(),
+ reducer = ::reduce,
+ middleware = middleware
+)
+
+private fun reduce(syncState: SyncState, syncAction: SyncAction): SyncState {
+ return when (syncAction) {
+ is SyncAction.UpdateSyncStatus -> syncState.copy(status = syncAction.status)
+ is SyncAction.UpdateAccount -> syncState.copy(account = syncAction.account)
+ is SyncAction.UpdateDeviceConstellation ->
+ syncState.copy(constellationState = syncAction.deviceConstellation)
+ }
+}
diff --git a/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt b/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt
new file mode 100644
index 00000000000..08f356d8e82
--- /dev/null
+++ b/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt
@@ -0,0 +1,130 @@
+/* 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.service.fxa.store
+
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import mozilla.components.concept.sync.AccountObserver
+import mozilla.components.concept.sync.AuthType
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.DeviceConstellationObserver
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.Profile
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.sync.SyncStatusObserver
+import java.lang.Exception
+
+/**
+ * Connections an [FxaAccountManager] with a [SyncStore], so that updates to Sync
+ * state can be observed.
+ *
+ * @param store The [SyncStore] to publish state updates based on [fxaAccountManager] observations.
+ * @param fxaAccountManager Account manager that is used to interact with Sync backends.
+ * @param lifecycleOwner The lifecycle owner that will tie to the when account manager observations.
+ * Recommended that this be an Application or at minimum a persistent Activity.
+ * @param autoPause Whether the account manager observations will stop between onPause and onResume.
+ * @param coroutineScope Scope used to launch various suspending operations.
+ */
+class SyncStoreSupport(
+ private val store: SyncStore,
+ private val fxaAccountManager: Lazy,
+ private val lifecycleOwner: LifecycleOwner,
+ private val autoPause: Boolean = false,
+ private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
+) {
+ /**
+ * Initialize the integration. This will cause it to register itself as an observer
+ * of the [FxaAccountManager] and begin dispatching [SyncStore] updates.
+ */
+ fun initialize() {
+ val accountManager = fxaAccountManager.value
+ accountManager.registerForSyncEvents(
+ AccountSyncObserver(store),
+ owner = lifecycleOwner,
+ autoPause = autoPause
+ )
+
+ val accountObserver = FxaAccountObserver(
+ store,
+ ConstellationObserver(store),
+ lifecycleOwner,
+ autoPause,
+ coroutineScope
+ )
+ accountManager.register(accountObserver, owner = lifecycleOwner, autoPause = autoPause)
+ }
+}
+
+/**
+ * Maps various [SyncStatusObserver] callbacks to [SyncAction] dispatches.
+ *
+ * @param store The [SyncStore] that updates will be dispatched to.
+ */
+internal class AccountSyncObserver(private val store: SyncStore) : SyncStatusObserver {
+ override fun onStarted() {
+ store.dispatch(SyncAction.UpdateSyncStatus(SyncStatus.Started))
+ }
+
+ override fun onIdle() {
+ store.dispatch(SyncAction.UpdateSyncStatus(SyncStatus.Idle))
+ }
+
+ override fun onError(error: Exception?) {
+ store.dispatch(SyncAction.UpdateSyncStatus(SyncStatus.Error))
+ }
+}
+
+/**
+ * Maps various [AccountObserver] callbacks to [SyncAction] dispatches.
+ *
+ * @param store The [SyncStore] that updates will be dispatched to.
+ * @param deviceConstellationObserver Will be registered as an observer to any constellations
+ * received in [AccountObserver.onAuthenticated].
+ *
+ * See [SyncStoreSupport] for the rest of the param definitions.
+ */
+internal class FxaAccountObserver(
+ private val store: SyncStore,
+ private val deviceConstellationObserver: DeviceConstellationObserver,
+ private val lifecycleOwner: LifecycleOwner,
+ private val autoPause: Boolean,
+ private val coroutineScope: CoroutineScope,
+) : AccountObserver {
+ override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
+ account.deviceConstellation().registerDeviceObserver(
+ deviceConstellationObserver,
+ owner = lifecycleOwner,
+ autoPause = autoPause
+ )
+ coroutineScope.launch {
+ val syncAccount = account.getProfile()?.toAccount(account) ?: return@launch
+ store.dispatch(SyncAction.UpdateAccount(syncAccount))
+ }
+ }
+}
+
+/**
+ * Maps various [DeviceConstellationObserver] callbacks to [SyncAction] dispatches.
+ *
+ * @param store The [SyncStore] that updates will be dispatched to.
+ */
+internal class ConstellationObserver(private val store: SyncStore) : DeviceConstellationObserver {
+ override fun onDevicesUpdate(constellation: ConstellationState) {
+ store.dispatch(SyncAction.UpdateDeviceConstellation(constellation))
+ }
+}
+
+// Could be refactored to use a context receiver once 1.6.2 upgrade lands
+private fun Profile.toAccount(oAuthAccount: OAuthAccount): Account =
+ Account(
+ uid = uid,
+ email = email,
+ avatar = avatar,
+ displayName = displayName,
+ currentDeviceId = oAuthAccount.getCurrentDeviceId(),
+ sessionToken = oAuthAccount.getSessionToken(),
+ )
diff --git a/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt b/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt
index c3f76552230..7a8fd1b286e 100644
--- a/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt
+++ b/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt
@@ -7,9 +7,10 @@ package mozilla.components.service.fxa
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.base.crash.CrashReporting
import mozilla.components.concept.sync.AccessTokenInfo
import mozilla.components.concept.sync.AccountEventsObserver
@@ -112,6 +113,7 @@ internal open class TestableFxaAccountManager(
const val EXPECTED_AUTH_STATE = "goodAuthState"
const val UNEXPECTED_AUTH_STATE = "badAuthState"
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class FxaAccountManagerTest {
@@ -185,7 +187,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `migrating an account via copyAccountAsync - creating a new session token`() = runBlocking {
+ fun `migrating an account via copyAccountAsync - creating a new session token`() = runTest {
// We'll test three scenarios:
// - hitting a network issue during migration
// - hitting an auth issue during migration (bad credentials)
@@ -257,7 +259,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `migrating an account via migrateAccountAsync - reusing existing session token`() = runBlocking {
+ fun `migrating an account via migrateAccountAsync - reusing existing session token`() = runTest {
// We'll test three scenarios:
// - hitting a network issue during migration
// - hitting an auth issue during migration (bad credentials)
@@ -329,7 +331,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `migrating an account via migrateAccountAsync - retry scenario`() = runBlocking {
+ fun `migrating an account via migrateAccountAsync - retry scenario`() = runTest {
val accountStorage: AccountStorage = mock()
val profile = Profile("testUid", "test@example.com", null, "Test Profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
@@ -388,7 +390,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `restored account has an in-flight migration, retries and fails`() = runBlocking {
+ fun `restored account has an in-flight migration, retries and fails`() = runTest {
val accountStorage: AccountStorage = mock()
val profile = Profile("testUid", "test@example.com", null, "Test Profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
@@ -425,7 +427,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `restored account has an in-flight migration, retries and succeeds`() = runBlocking {
+ fun `restored account has an in-flight migration, retries and succeeds`() = runTest {
val accountStorage: AccountStorage = mock()
val profile = Profile("testUid", "test@example.com", null, "Test Profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
@@ -459,7 +461,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `restored account state persistence`() = runBlocking {
+ fun `restored account state persistence`() = runTest {
val accountStorage: AccountStorage = mock()
val profile = Profile("testUid", "test@example.com", null, "Test Profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
@@ -491,7 +493,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `restored account state persistence, finalizeDevice hit an intermittent error`() = runBlocking {
+ fun `restored account state persistence, finalizeDevice hit an intermittent error`() = runTest {
val accountStorage: AccountStorage = mock()
val profile = Profile("testUid", "test@example.com", null, "Test Profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
@@ -526,7 +528,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `restored account state persistence, hit an auth error`() = runBlocking {
+ fun `restored account state persistence, hit an auth error`() = runTest {
val accountStorage: AccountStorage = mock()
val profile = Profile("testUid", "test@example.com", null, "Test Profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
@@ -561,7 +563,7 @@ class FxaAccountManagerTest {
}
@Test(expected = FxaPanicException::class)
- fun `restored account state persistence, hit an fxa panic which is re-thrown`() = runBlocking {
+ fun `restored account state persistence, hit an fxa panic which is re-thrown`() = runTest {
val accountStorage: AccountStorage = mock()
val profile = Profile("testUid", "test@example.com", null, "Test Profile")
val constellation: DeviceConstellation = mock()
@@ -594,7 +596,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `newly authenticated account state persistence`() = runBlocking {
+ fun `newly authenticated account state persistence`() = runTest {
val accountStorage: AccountStorage = mock()
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
@@ -636,7 +638,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `auth state verification while finishing authentication`() = runBlocking {
+ fun `auth state verification while finishing authentication`() = runTest {
val accountStorage: AccountStorage = mock()
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
@@ -793,7 +795,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `error reading persisted account`() = runBlocking {
+ fun `error reading persisted account`() = runTest {
val accountStorage = mock()
val readException = FxaNetworkException("pretend we failed to fetch the account")
`when`(accountStorage.read()).thenThrow(readException)
@@ -827,7 +829,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `no persisted account`() = runBlocking {
+ fun `no persisted account`() = runTest {
val accountStorage = mock()
// There's no account at the start.
`when`(accountStorage.read()).thenReturn(null)
@@ -856,7 +858,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `with persisted account and profile`() = runBlocking {
+ fun `with persisted account and profile`() = runTest {
val accountStorage = mock()
val mockAccount: OAuthAccount = mock()
val constellation: DeviceConstellation = mock()
@@ -924,7 +926,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `happy authentication and profile flow`() = runBlocking {
+ fun `happy authentication and profile flow`() = runTest {
val mockAccount: OAuthAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
@@ -968,7 +970,7 @@ class FxaAccountManagerTest {
}
@Test(expected = FxaPanicException::class)
- fun `fxa panic during initDevice flow`() = runBlocking {
+ fun `fxa panic during initDevice flow`() = runTest {
val mockAccount: OAuthAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
@@ -995,7 +997,7 @@ class FxaAccountManagerTest {
}
@Test(expected = FxaPanicException::class)
- fun `fxa panic during pairing flow`() = runBlocking {
+ fun `fxa panic during pairing flow`() = runTest {
val mockAccount: OAuthAccount = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(mock())
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
@@ -1023,7 +1025,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `happy pairing authentication and profile flow`() = runBlocking {
+ fun `happy pairing authentication and profile flow`() = runTest {
val mockAccount: OAuthAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
@@ -1059,7 +1061,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `repeated unfinished authentication attempts succeed`() = runBlocking {
+ fun `repeated unfinished authentication attempts succeed`() = runTest {
val mockAccount: OAuthAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
@@ -1102,7 +1104,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `unhappy authentication flow`() = runBlocking {
+ fun `unhappy authentication flow`() = runTest {
val accountStorage = mock()
val mockAccount: OAuthAccount = mock()
val constellation: DeviceConstellation = mock()
@@ -1149,7 +1151,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `unhappy pairing authentication flow`() = runBlocking {
+ fun `unhappy pairing authentication flow`() = runTest {
val accountStorage = mock()
val mockAccount: OAuthAccount = mock()
val constellation: DeviceConstellation = mock()
@@ -1196,7 +1198,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `authentication issues are propagated via AccountObserver`() = runBlocking {
+ fun `authentication issues are propagated via AccountObserver`() = runTest {
val mockAccount: OAuthAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
@@ -1249,7 +1251,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `authentication issues are recoverable via checkAuthorizationState`() = runBlocking {
+ fun `authentication issues are recoverable via checkAuthorizationState`() = runTest {
val mockAccount: OAuthAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
@@ -1295,7 +1297,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `authentication recovery flow has a circuit breaker`() = runBlocking {
+ fun `authentication recovery flow has a circuit breaker`() = runTest {
val mockAccount: OAuthAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
@@ -1384,7 +1386,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `unhappy profile fetching flow`() = runBlocking {
+ fun `unhappy profile fetching flow`() = runTest {
val accountStorage = mock()
val mockAccount: OAuthAccount = mock()
val constellation: DeviceConstellation = mock()
@@ -1448,7 +1450,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `profile fetching flow hit an unrecoverable auth problem`() = runBlocking {
+ fun `profile fetching flow hit an unrecoverable auth problem`() = runTest {
val accountStorage = mock()
val mockAccount: OAuthAccount = mock()
val constellation: DeviceConstellation = mock()
@@ -1507,7 +1509,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `profile fetching flow hit an unrecoverable auth problem for which we can't determine a recovery state`() = runBlocking {
+ fun `profile fetching flow hit an unrecoverable auth problem for which we can't determine a recovery state`() = runTest {
val accountStorage = mock()
val mockAccount: OAuthAccount = mock()
val constellation: DeviceConstellation = mock()
@@ -1566,7 +1568,7 @@ class FxaAccountManagerTest {
}
@Test
- fun `profile fetching flow hit a recoverable auth problem`() = runBlocking {
+ fun `profile fetching flow hit a recoverable auth problem`() = runTest {
val accountStorage = mock()
val mockAccount: OAuthAccount = mock()
val constellation: DeviceConstellation = mock()
@@ -1645,7 +1647,7 @@ class FxaAccountManagerTest {
}
@Test(expected = FxaPanicException::class)
- fun `profile fetching flow hit an fxa panic, which is re-thrown`() = runBlocking {
+ fun `profile fetching flow hit an fxa panic, which is re-thrown`() = runTest {
val accountStorage = mock()
val mockAccount: OAuthAccount = mock()
val constellation: DeviceConstellation = mock()
@@ -1797,9 +1799,7 @@ class FxaAccountManagerTest {
manager.register(accountObserver)
- runBlocking(coroutineContext) {
- manager.start()
- }
+ manager.start()
return manager
}
diff --git a/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaDeviceConstellationTest.kt b/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaDeviceConstellationTest.kt
index 5e91700fb04..40febb74a3e 100644
--- a/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaDeviceConstellationTest.kt
+++ b/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaDeviceConstellationTest.kt
@@ -11,7 +11,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.plus
-import kotlinx.coroutines.runBlocking
import mozilla.appservices.fxaclient.FxaException
import mozilla.appservices.fxaclient.IncomingDeviceCommand
import mozilla.appservices.fxaclient.SendTabPayload
@@ -34,6 +33,7 @@ import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -75,7 +75,7 @@ class FxaDeviceConstellationTest {
}
@Test
- fun `finalize device`() = runBlocking(coroutinesTestRule.testDispatcher) {
+ fun `finalize device`() = runTestOnMain {
fun expectedFinalizeAction(authType: AuthType): FxaDeviceConstellation.DeviceFinalizeAction = when (authType) {
AuthType.Existing -> FxaDeviceConstellation.DeviceFinalizeAction.EnsureCapabilities
AuthType.Signin -> FxaDeviceConstellation.DeviceFinalizeAction.Initialize
@@ -117,7 +117,7 @@ class FxaDeviceConstellationTest {
@Test
@ExperimentalCoroutinesApi
- fun `updating device name`() = runBlocking(coroutinesTestRule.testDispatcher) {
+ fun `updating device name`() = runTestOnMain {
val currentDevice = testDevice("currentTestDevice", true)
`when`(account.getDevices()).thenReturn(arrayOf(currentDevice))
@@ -158,7 +158,7 @@ class FxaDeviceConstellationTest {
@Test
@ExperimentalCoroutinesApi
- fun `set device push subscription`() = runBlocking(coroutinesTestRule.testDispatcher) {
+ fun `set device push subscription`() = runTestOnMain {
val subscription = DevicePushSubscription("http://endpoint.com", "pk", "auth key")
constellation.setDevicePushSubscription(subscription)
@@ -167,7 +167,7 @@ class FxaDeviceConstellationTest {
@Test
@ExperimentalCoroutinesApi
- fun `process raw device command`() = runBlocking(coroutinesTestRule.testDispatcher) {
+ fun `process raw device command`() = runTestOnMain {
// No commands, no observer.
`when`(account.handlePushMessage("raw events payload")).thenReturn(emptyArray())
assertTrue(constellation.processRawEvent("raw events payload"))
@@ -205,7 +205,7 @@ class FxaDeviceConstellationTest {
}
@Test
- fun `send command to device`() = runBlocking(coroutinesTestRule.testDispatcher) {
+ fun `send command to device`() = runTestOnMain {
`when`(account.gatherTelemetry()).thenReturn("{}")
assertTrue(
constellation.sendCommandToDevice(
@@ -217,7 +217,7 @@ class FxaDeviceConstellationTest {
}
@Test
- fun `send command to device will report exceptions`() = runBlocking(coroutinesTestRule.testDispatcher) {
+ fun `send command to device will report exceptions`() = runTestOnMain {
val exception = FxaException.Other("")
val exceptionCaptor = argumentCaptor()
doAnswer { throw exception }.`when`(account).sendSingleTab(any(), any(), any())
@@ -232,7 +232,7 @@ class FxaDeviceConstellationTest {
}
@Test
- fun `send command to device won't report network exceptions`() = runBlocking(coroutinesTestRule.testDispatcher) {
+ fun `send command to device won't report network exceptions`() = runTestOnMain {
val exception = FxaException.Network("timeout!")
doAnswer { throw exception }.`when`(account).sendSingleTab(any(), any(), any())
@@ -247,7 +247,7 @@ class FxaDeviceConstellationTest {
@Test
@ExperimentalCoroutinesApi
- fun `refreshing constellation`() = runBlocking(coroutinesTestRule.testDispatcher) {
+ fun `refreshing constellation`() = runTestOnMain {
// No devices, no observers.
`when`(account.getDevices()).thenReturn(emptyArray())
@@ -332,7 +332,7 @@ class FxaDeviceConstellationTest {
@Test
@ExperimentalCoroutinesApi
- fun `polling for commands triggers observers`() = runBlocking(coroutinesTestRule.testDispatcher) {
+ fun `polling for commands triggers observers`() = runTestOnMain {
// No commands, no observers.
`when`(account.gatherTelemetry()).thenReturn("{}")
`when`(account.pollDeviceCommands()).thenReturn(emptyArray())
diff --git a/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/UtilsKtTest.kt b/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/UtilsKtTest.kt
index 9b11d99491c..cdb4f0d585a 100644
--- a/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/UtilsKtTest.kt
+++ b/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/UtilsKtTest.kt
@@ -4,7 +4,8 @@
package mozilla.components.service.fxa
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.sync.AuthFlowUrl
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.ServiceResult
@@ -25,9 +26,10 @@ import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoInteractions
+@ExperimentalCoroutinesApi // for runTest
class UtilsKtTest {
@Test
- fun `handleFxaExceptions form 1 returns correct data back`() = runBlocking {
+ fun `handleFxaExceptions form 1 returns correct data back`() = runTest {
assertEquals(
1,
handleFxaExceptions(
@@ -52,7 +54,7 @@ class UtilsKtTest {
}
@Test
- fun `handleFxaExceptions form 1 does not swallow non-panics`() = runBlocking {
+ fun `handleFxaExceptions form 1 does not swallow non-panics`() = runTest {
val accountManager: FxaAccountManager = mock()
GlobalAccountManager.setInstance(accountManager)
@@ -113,7 +115,7 @@ class UtilsKtTest {
}
@Test(expected = IllegalStateException::class)
- fun `handleFxaExceptions form 1 re-throws non-fxa exceptions`() = runBlocking {
+ fun `handleFxaExceptions form 1 re-throws non-fxa exceptions`() = runTest {
handleFxaExceptions(
mock(), "test op",
{
@@ -124,7 +126,7 @@ class UtilsKtTest {
}
@Test(expected = FxaPanicException::class)
- fun `handleFxaExceptions form 1 re-throws fxa panic exceptions`() = runBlocking {
+ fun `handleFxaExceptions form 1 re-throws fxa panic exceptions`() = runTest {
handleFxaExceptions(
mock(), "test op",
{
@@ -135,7 +137,7 @@ class UtilsKtTest {
}
@Test
- fun `handleFxaExceptions form 2 works`() = runBlocking {
+ fun `handleFxaExceptions form 2 works`() = runTest {
val accountManager: FxaAccountManager = mock()
GlobalAccountManager.setInstance(accountManager)
@@ -173,7 +175,7 @@ class UtilsKtTest {
}
@Test(expected = IllegalStateException::class)
- fun `handleFxaExceptions form 2 re-throws non-fxa exceptions`() = runBlocking {
+ fun `handleFxaExceptions form 2 re-throws non-fxa exceptions`() = runTest {
val accountManager: FxaAccountManager = mock()
GlobalAccountManager.setInstance(accountManager)
@@ -184,7 +186,7 @@ class UtilsKtTest {
}
@Test(expected = FxaPanicException::class)
- fun `handleFxaExceptions form 2 re-throws fxa panic exceptions`() = runBlocking {
+ fun `handleFxaExceptions form 2 re-throws fxa panic exceptions`() = runTest {
val accountManager: FxaAccountManager = mock()
GlobalAccountManager.setInstance(accountManager)
@@ -196,7 +198,7 @@ class UtilsKtTest {
}
@Test
- fun `handleFxaExceptions form 3 works`() = runBlocking {
+ fun `handleFxaExceptions form 3 works`() = runTest {
val accountManager: FxaAccountManager = mock()
GlobalAccountManager.setInstance(accountManager)
@@ -238,7 +240,7 @@ class UtilsKtTest {
}
@Test(expected = IllegalStateException::class)
- fun `handleFxaExceptions form 3 re-throws non-fxa exceptions`() = runBlocking {
+ fun `handleFxaExceptions form 3 re-throws non-fxa exceptions`() = runTest {
val accountManager: FxaAccountManager = mock()
GlobalAccountManager.setInstance(accountManager)
@@ -249,7 +251,7 @@ class UtilsKtTest {
}
@Test(expected = FxaPanicException::class)
- fun `handleFxaExceptions form 3 re-throws fxa panic exceptions`() = runBlocking {
+ fun `handleFxaExceptions form 3 re-throws fxa panic exceptions`() = runTest {
val accountManager: FxaAccountManager = mock()
GlobalAccountManager.setInstance(accountManager)
@@ -260,7 +262,7 @@ class UtilsKtTest {
}
@Test
- fun `withRetries immediate success`() = runBlocking {
+ fun `withRetries immediate success`() = runTest {
when (val res = withRetries(mock(), 3) { true }) {
is Result.Success -> assertTrue(res.value)
is Result.Failure -> fail()
@@ -277,7 +279,7 @@ class UtilsKtTest {
}
@Test
- fun `withRetries immediate failure`() = runBlocking {
+ fun `withRetries immediate failure`() = runTest {
when (withRetries(mock(), 3) { false }) {
is Result.Success -> fail()
is Result.Failure -> {}
@@ -289,7 +291,7 @@ class UtilsKtTest {
}
@Test
- fun `withRetries eventual success`() = runBlocking {
+ fun `withRetries eventual success`() = runTest {
val eventual = SucceedOn(2, true)
when (val res = withRetries(mock(), 5) { eventual.nullFailure() }) {
is Result.Success -> {
@@ -309,7 +311,7 @@ class UtilsKtTest {
}
@Test
- fun `withRetries eventual failure`() = runBlocking {
+ fun `withRetries eventual failure`() = runTest {
val eventual = SucceedOn(6, true)
when (withRetries(mock(), 5) { eventual.nullFailure() }) {
is Result.Success -> fail()
@@ -327,7 +329,7 @@ class UtilsKtTest {
}
@Test
- fun `withServiceRetries immediate success`() = runBlocking {
+ fun `withServiceRetries immediate success`() = runTest {
when (withServiceRetries(mock(), 3, suspend { ServiceResult.Ok })) {
is ServiceResult.Ok -> {}
else -> fail()
@@ -335,7 +337,7 @@ class UtilsKtTest {
}
@Test
- fun `withServiceRetries generic failure keeps retrying`() = runBlocking {
+ fun `withServiceRetries generic failure keeps retrying`() = runTest {
// keeps retrying on generic error
val eventual = SucceedOn(0, ServiceResult.Ok, ServiceResult.OtherError)
when (withServiceRetries(mock(), 3) { eventual.reifiedFailure() }) {
@@ -347,7 +349,7 @@ class UtilsKtTest {
}
@Test
- fun `withServiceRetries auth failure short circuit`() = runBlocking {
+ fun `withServiceRetries auth failure short circuit`() = runTest {
// keeps retrying on generic error
val eventual = SucceedOn(0, ServiceResult.Ok, ServiceResult.AuthError)
when (withServiceRetries(mock(), 3) { eventual.reifiedFailure() }) {
@@ -359,7 +361,7 @@ class UtilsKtTest {
}
@Test
- fun `withServiceRetries eventual success`() = runBlocking {
+ fun `withServiceRetries eventual success`() = runTest {
val eventual = SucceedOn(3, ServiceResult.Ok, ServiceResult.OtherError)
when (withServiceRetries(mock(), 5) { eventual.reifiedFailure() }) {
is ServiceResult.Ok -> {
@@ -370,7 +372,7 @@ class UtilsKtTest {
}
@Test
- fun `as auth flow pairing`() = runBlocking {
+ fun `as auth flow pairing`() = runTest {
val account: OAuthAccount = mock()
val authFlowUrl: AuthFlowUrl = mock()
`when`(account.beginPairingFlow(eq("http://pairing.url"), eq(emptySet()), anyString())).thenReturn(authFlowUrl)
@@ -379,7 +381,7 @@ class UtilsKtTest {
}
@Test
- fun `as auth flow regular`() = runBlocking {
+ fun `as auth flow regular`() = runTest {
val account: OAuthAccount = mock()
val authFlowUrl: AuthFlowUrl = mock()
`when`(account.beginOAuthFlow(eq(emptySet()), anyString())).thenReturn(authFlowUrl)
diff --git a/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/GlobalAccountManagerTest.kt b/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/GlobalAccountManagerTest.kt
index 28fe72f8321..7d9b64fc0c4 100644
--- a/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/GlobalAccountManagerTest.kt
+++ b/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/GlobalAccountManagerTest.kt
@@ -4,14 +4,16 @@
package mozilla.components.service.fxa.manager
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.support.test.mock
import org.junit.Test
import org.mockito.Mockito
class GlobalAccountManagerTest {
+ @ExperimentalCoroutinesApi
@Test
- fun `GlobalAccountManager authError processing`() = runBlocking {
+ fun `GlobalAccountManager authError processing`() = runTest {
val manager: FxaAccountManager = mock()
GlobalAccountManager.setInstance(manager)
diff --git a/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt b/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt
new file mode 100644
index 00000000000..3bf3b9b4b1c
--- /dev/null
+++ b/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt
@@ -0,0 +1,162 @@
+package mozilla.components.service.fxa.store
+
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.sync.Avatar
+import mozilla.components.concept.sync.ConstellationState
+import mozilla.components.concept.sync.DeviceConstellation
+import mozilla.components.concept.sync.OAuthAccount
+import mozilla.components.concept.sync.Profile
+import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.support.test.any
+import mozilla.components.support.test.coMock
+import mozilla.components.support.test.eq
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+import java.lang.Exception
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class SyncStoreSupportTest {
+
+ private val accountManager = mock()
+ private val lifecycleOwner = mock()
+ private val autoPause = false
+ private val coroutineScope = TestScope()
+
+ private lateinit var store: SyncStore
+ private lateinit var syncObserver: AccountSyncObserver
+ private lateinit var constellationObserver: ConstellationObserver
+ private lateinit var accountObserver: FxaAccountObserver
+ private lateinit var integration: SyncStoreSupport
+
+ @Before
+ fun setup() {
+ store = SyncStore()
+ syncObserver = AccountSyncObserver(store)
+ constellationObserver = ConstellationObserver(store)
+ accountObserver = FxaAccountObserver(
+ store = store,
+ deviceConstellationObserver = constellationObserver,
+ lifecycleOwner = lifecycleOwner,
+ autoPause = autoPause,
+ coroutineScope = coroutineScope,
+ )
+
+ integration = SyncStoreSupport(
+ store = store,
+ fxaAccountManager = lazyOf(accountManager),
+ lifecycleOwner = lifecycleOwner,
+ autoPause = autoPause,
+ coroutineScope = coroutineScope,
+ )
+ }
+
+ @Test
+ fun `GIVEN integration WHEN initialize is called THEN observers registered`() {
+ integration.initialize()
+
+ verify(accountManager).registerForSyncEvents(any(), eq(lifecycleOwner), eq(autoPause))
+ verify(accountManager).register(any(), eq(lifecycleOwner), eq(autoPause))
+ }
+
+ @Test
+ fun `GIVEN sync observer WHEN onStarted observed THEN sync status updated`() {
+ syncObserver.onStarted()
+
+ store.waitUntilIdle()
+ assertEquals(SyncStatus.Started, store.state.status)
+ }
+
+ @Test
+ fun `GIVEN sync observer WHEN onIdle observed THEN sync status updated`() {
+ syncObserver.onIdle()
+
+ store.waitUntilIdle()
+ assertEquals(SyncStatus.Idle, store.state.status)
+ }
+
+ @Test
+ fun `GIVEN sync observer WHEN onError observed THEN sync status updated`() {
+ syncObserver.onError(Exception())
+
+ store.waitUntilIdle()
+ assertEquals(SyncStatus.Error, store.state.status)
+ }
+
+ @Test
+ fun `GIVEN account observer WHEN onAuthenticated observed THEN device observer registered`() {
+ val constellation = mock()
+ val account = mock {
+ whenever(deviceConstellation()).thenReturn(constellation)
+ }
+
+ accountObserver.onAuthenticated(account, mock())
+
+ verify(constellation).registerDeviceObserver(constellationObserver, lifecycleOwner, autoPause)
+ }
+
+ @Test
+ fun `GIVEN account observer WHEN onAuthenticated observed with profile THEN account state updated`() = coroutineScope.runTest {
+ val profile = generateProfile()
+ val constellation = mock()
+ val account = coMock {
+ whenever(deviceConstellation()).thenReturn(constellation)
+ whenever(getCurrentDeviceId()).thenReturn("id")
+ whenever(getSessionToken()).thenReturn("token")
+ whenever(getProfile()).thenReturn(profile)
+ }
+
+ accountObserver.onAuthenticated(account, mock())
+ runCurrent()
+
+ val expected = Account(
+ profile.uid,
+ profile.email,
+ profile.avatar,
+ profile.displayName,
+ "id",
+ "token"
+ )
+ store.waitUntilIdle()
+ assertEquals(expected, store.state.account)
+ }
+
+ @Test
+ fun `GIVEN account observer WHEN onAuthenticated observed without profile THEN account not updated`() = coroutineScope.runTest {
+ val constellation = mock()
+ val account = coMock {
+ whenever(deviceConstellation()).thenReturn(constellation)
+ whenever(getProfile()).thenReturn(null)
+ }
+
+ accountObserver.onAuthenticated(account, mock())
+ runCurrent()
+
+ store.waitUntilIdle()
+ assertEquals(null, store.state.account)
+ }
+
+ @Test
+ fun `GIVEN device observer WHEN onDevicesUpdate observed THEN constellation state updated`() {
+ val constellation = mock()
+ constellationObserver.onDevicesUpdate(constellation)
+
+ store.waitUntilIdle()
+ assertEquals(constellation, store.state.constellationState)
+ }
+
+ private fun generateProfile(
+ uid: String = "uid",
+ email: String = "email",
+ avatar: Avatar = Avatar("url", true),
+ displayName: String = "displayName",
+ ) = Profile(uid, email, avatar, displayName)
+}
diff --git a/components/service/location/build.gradle b/components/service/location/build.gradle
index 442f25d19f5..2b27a4f40e0 100644
--- a/components/service/location/build.gradle
+++ b/components/service/location/build.gradle
@@ -35,6 +35,7 @@ dependencies {
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
testImplementation Dependencies.testing_mockwebserver
+ testImplementation Dependencies.testing_coroutines
testImplementation project(':lib-fetch-httpurlconnection')
}
diff --git a/components/service/location/src/test/java/mozilla/components/service/location/LocationServiceTest.kt b/components/service/location/src/test/java/mozilla/components/service/location/LocationServiceTest.kt
index 4477196ffd9..eca31d268f9 100644
--- a/components/service/location/src/test/java/mozilla/components/service/location/LocationServiceTest.kt
+++ b/components/service/location/src/test/java/mozilla/components/service/location/LocationServiceTest.kt
@@ -5,13 +5,16 @@
package mozilla.components.service.location
import junit.framework.TestCase.assertNull
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
import org.junit.Test
class LocationServiceTest {
+ @ExperimentalCoroutinesApi
@Test
fun `dummy implementation returns null`() {
- runBlocking {
+ runTest(UnconfinedTestDispatcher()) {
assertNull(LocationService.dummy().fetchRegion(false))
assertNull(LocationService.dummy().fetchRegion(true))
assertNull(LocationService.dummy().fetchRegion(false))
diff --git a/components/service/location/src/test/java/mozilla/components/service/location/MozillaLocationServiceTest.kt b/components/service/location/src/test/java/mozilla/components/service/location/MozillaLocationServiceTest.kt
index 05c555e0336..d95c64c7c24 100644
--- a/components/service/location/src/test/java/mozilla/components/service/location/MozillaLocationServiceTest.kt
+++ b/components/service/location/src/test/java/mozilla/components/service/location/MozillaLocationServiceTest.kt
@@ -5,7 +5,8 @@
package mozilla.components.service.location
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.MutableHeaders
import mozilla.components.concept.fetch.Request
@@ -30,6 +31,7 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import java.io.IOException
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class MozillaLocationServiceTest {
@Before
@@ -39,7 +41,7 @@ class MozillaLocationServiceTest {
}
@Test
- fun `WHEN calling fetchRegion AND the service returns a region THEN a Region object is returned`() {
+ fun `WHEN calling fetchRegion AND the service returns a region THEN a Region object is returned`() = runTest {
val server = MockWebServer()
server.enqueue(MockResponse().setBody("{\"country_name\": \"Germany\", \"country_code\": \"DE\"}"))
@@ -53,7 +55,7 @@ class MozillaLocationServiceTest {
serviceUrl = server.url("/").toString()
)
- val region = runBlocking { service.fetchRegion() }
+ val region = service.fetchRegion()
assertNotNull(region!!)
@@ -69,18 +71,18 @@ class MozillaLocationServiceTest {
}
@Test
- fun `WHEN client throws IOException THEN the returned region is null`() {
+ fun `WHEN client throws IOException THEN the returned region is null`() = runTest {
val client: Client = mock()
doThrow(IOException()).`when`(client).fetch(any())
val service = MozillaLocationService(testContext, client, apiKey = "test")
- val region = runBlocking { service.fetchRegion() }
+ val region = service.fetchRegion()
assertNull(region)
}
@Test
- fun `WHEN fetching region THEN request is sent to the location service`() {
+ fun `WHEN fetching region THEN request is sent to the location service`() = runTest {
val client: Client = mock()
val response = Response(
url = "http://example.org",
@@ -91,7 +93,7 @@ class MozillaLocationServiceTest {
doReturn(response).`when`(client).fetch(any())
val service = MozillaLocationService(testContext, client, apiKey = "test")
- val region = runBlocking { service.fetchRegion() }
+ val region = service.fetchRegion()
assertNotNull(region!!)
@@ -106,7 +108,7 @@ class MozillaLocationServiceTest {
}
@Test
- fun `WHEN fetching region AND service returns 404 THEN region is null`() {
+ fun `WHEN fetching region AND service returns 404 THEN region is null`() = runTest {
val client: Client = mock()
val response = Response(
url = "http://example.org",
@@ -117,13 +119,13 @@ class MozillaLocationServiceTest {
doReturn(response).`when`(client).fetch(any())
val service = MozillaLocationService(testContext, client, apiKey = "test")
- val region = runBlocking { service.fetchRegion() }
+ val region = service.fetchRegion()
assertNull(region)
}
@Test
- fun `WHEN fetching region AND service returns 500 THEN region is null`() {
+ fun `WHEN fetching region AND service returns 500 THEN region is null`() = runTest {
val client: Client = mock()
val response = Response(
url = "http://example.org",
@@ -134,13 +136,13 @@ class MozillaLocationServiceTest {
doReturn(response).`when`(client).fetch(any())
val service = MozillaLocationService(testContext, client, apiKey = "test")
- val region = runBlocking { service.fetchRegion() }
+ val region = service.fetchRegion()
assertNull(region)
}
@Test
- fun `WHEN fetching region AND service returns broken JSON THEN region is null`() {
+ fun `WHEN fetching region AND service returns broken JSON THEN region is null`() = runTest {
val client: Client = mock()
val response = Response(
url = "http://example.org",
@@ -151,13 +153,13 @@ class MozillaLocationServiceTest {
doReturn(response).`when`(client).fetch(any())
val service = MozillaLocationService(testContext, client, apiKey = "test")
- val region = runBlocking { service.fetchRegion() }
+ val region = service.fetchRegion()
assertNull(region)
}
@Test
- fun `WHEN fetching region AND service returns empty JSON object THEN region is null`() {
+ fun `WHEN fetching region AND service returns empty JSON object THEN region is null`() = runTest {
val client: Client = mock()
val response = Response(
url = "http://example.org",
@@ -168,13 +170,13 @@ class MozillaLocationServiceTest {
doReturn(response).`when`(client).fetch(any())
val service = MozillaLocationService(testContext, client, apiKey = "test")
- val region = runBlocking { service.fetchRegion() }
+ val region = service.fetchRegion()
assertNull(region)
}
@Test
- fun `WHEN fetching region AND service returns incomplete JSON THEN region is null`() {
+ fun `WHEN fetching region AND service returns incomplete JSON THEN region is null`() = runTest {
val client: Client = mock()
val response = Response(
url = "http://example.org",
@@ -185,13 +187,13 @@ class MozillaLocationServiceTest {
doReturn(response).`when`(client).fetch(any())
val service = MozillaLocationService(testContext, client, apiKey = "test")
- val region = runBlocking { service.fetchRegion() }
+ val region = service.fetchRegion()
assertNull(region)
}
@Test
- fun `WHEN fetching region for the second time THEN region is read from cache`() {
+ fun `WHEN fetching region for the second time THEN region is read from cache`() = runTest {
run {
val client: Client = mock()
val response = Response(
@@ -203,7 +205,7 @@ class MozillaLocationServiceTest {
doReturn(response).`when`(client).fetch(any())
val service = MozillaLocationService(testContext, client, apiKey = "test")
- val region = runBlocking { service.fetchRegion() }
+ val region = service.fetchRegion()
assertNotNull(region!!)
@@ -217,7 +219,7 @@ class MozillaLocationServiceTest {
val client: Client = mock()
val service = MozillaLocationService(testContext, client, apiKey = "test")
- val region = runBlocking { service.fetchRegion() }
+ val region = service.fetchRegion()
assertNotNull(region!!)
@@ -229,7 +231,7 @@ class MozillaLocationServiceTest {
}
@Test
- fun `WHEN fetching region for the second time and setting readFromCache = false THEN request is sent again`() {
+ fun `WHEN fetching region for the second time and setting readFromCache = false THEN request is sent again`() = runTest {
run {
val client: Client = mock()
val response = Response(
@@ -241,7 +243,7 @@ class MozillaLocationServiceTest {
doReturn(response).`when`(client).fetch(any())
val service = MozillaLocationService(testContext, client, apiKey = "test")
- val region = runBlocking { service.fetchRegion() }
+ val region = service.fetchRegion()
assertNotNull(region!!)
@@ -262,7 +264,7 @@ class MozillaLocationServiceTest {
doReturn(response).`when`(client).fetch(any())
val service = MozillaLocationService(testContext, client, apiKey = "test")
- val region = runBlocking { service.fetchRegion(readFromCache = false) }
+ val region = service.fetchRegion(readFromCache = false)
assertNotNull(region!!)
diff --git a/components/service/nimbus/src/main/res/values-skr/strings.xml b/components/service/nimbus/src/main/res/values-skr/strings.xml
new file mode 100644
index 00000000000..0135c35baf9
--- /dev/null
+++ b/components/service/nimbus/src/main/res/values-skr/strings.xml
@@ -0,0 +1,5 @@
+
+
+
+ اتھاں کوئی تجربے کائنی
+
diff --git a/components/service/pocket/README.md b/components/service/pocket/README.md
index 9d36ab67c8e..86c2e0ec752 100644
--- a/components/service/pocket/README.md
+++ b/components/service/pocket/README.md
@@ -1,13 +1,33 @@
# [Android Components](../../../README.md) > Service > Pocket
A library for easily getting Pocket recommendations that transparently handles downloading, caching and periodically refreshing Pocket data.
-Currently this supports Pocket recommended stories.
+
+Currently this supports:
+
+- Pocket recommended stories.
+- Pocket sponsored stories.
## Usage
-- Use PocketStoriesService#startPeriodicStoriesRefresh and PocketStoriesService#stopPeriodicStoriesRefresh
-as high up in the client app as possible (preferably in the Application object or in a single Activity) to ensure the
-background story refresh functionality works for the entirety of the app lifetime.
-- Use PocketStoriesService.getStories to get the current list of Pocket recommended stories.
+1. For Pocket recommended stories:
+ - Use `PocketStoriesService#startPeriodicStoriesRefresh` and `PocketStoriesService#stopPeriodicStoriesRefresh`
+ as high up in the client app as possible (preferably in the Application object or in a single Activity) to ensure the
+ background story refresh functionality works for the entirety of the app lifetime.
+ - Use `PocketStoriesService.getStories` to get the current list of Pocket recommended stories.
+
+2. For Pocket sponsored stories:
+ - Use `PocketStoriesService#startPeriodicSponsoredStoriesRefresh` and `PocketStoriesService#stopPeriodicSponsoredStoriesRefresh`
+ as high up in the client app as possible (preferably in the Application object or in a single Activity) to ensure the
+ background story refresh functionality works for the entirety of the app lifetime.
+ - Use `PocketStoriesService.getSponsoredStories` to get the current list of Pocket recommended stories.
+ - Use `PocketStoriesService,recordStoriesImpressions` to try and persist that a list of sponsored stories were shown to the user. (Safe to call even if those stories are not persisted).
+ - Use `PocketStoriesService.deleteProfile` to delete all server stored information about the device to which sponsored stories were previously downloaded. This may include data like network ip and application tokens.
+
+ ##### Pacing and rotating:
+ A new `PocketSponsoredStoryCaps` is available in the response from `PocketStoriesService.getSponsoredStories` which allows checking `currentImpressions`, `lifetimeCount`, `flightCount`, `flightPeriod` based on which the client can decide which stories to show.
+ All this is based on clients calling `PocketStoriesService,recordStoriesImpressions` to record new impressions in between application restarts.
+
+
+
### Setting up the dependency
diff --git a/components/service/pocket/build.gradle b/components/service/pocket/build.gradle
index c162c6efcb9..25e7dd000ee 100644
--- a/components/service/pocket/build.gradle
+++ b/components/service/pocket/build.gradle
@@ -12,6 +12,7 @@ android {
defaultConfig {
minSdkVersion config.minSdkVersion
targetSdkVersion config.targetSdkVersion
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
kapt {
arguments {
@@ -26,6 +27,11 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
+
+ sourceSets {
+ test.assets.srcDirs += files("$projectDir/schemas".toString())
+ androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
+ }
}
dependencies {
@@ -49,9 +55,19 @@ dependencies {
testImplementation Dependencies.testing_coroutines
testImplementation Dependencies.testing_mockito
testImplementation Dependencies.testing_robolectric
+ testImplementation Dependencies.androidx_room_testing
+ testImplementation Dependencies.androidx_work_testing
testImplementation project(':support-test')
testImplementation project(':lib-fetch-httpurlconnection')
+
+ androidTestImplementation project(':support-android-test')
+
+ androidTestImplementation Dependencies.androidx_room_testing
+ androidTestImplementation Dependencies.androidx_arch_core_testing
+ androidTestImplementation Dependencies.androidx_test_core
+ androidTestImplementation Dependencies.androidx_test_runner
+ androidTestImplementation Dependencies.androidx_test_rules
}
apply from: '../../../publish.gradle'
diff --git a/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/2.json b/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/2.json
new file mode 100644
index 00000000000..963deb138b3
--- /dev/null
+++ b/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/2.json
@@ -0,0 +1,120 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "1ea41b5cc0791d92dd8f0db8b387fe6c",
+ "entities": [
+ {
+ "tableName": "stories",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `imageUrl` TEXT NOT NULL, `publisher` TEXT NOT NULL, `category` TEXT NOT NULL, `timeToRead` INTEGER NOT NULL, `timesShown` INTEGER NOT NULL, PRIMARY KEY(`url`))",
+ "fields": [
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "imageUrl",
+ "columnName": "imageUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publisher",
+ "columnName": "publisher",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "category",
+ "columnName": "category",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timeToRead",
+ "columnName": "timeToRead",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timesShown",
+ "columnName": "timesShown",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "url"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "spocs",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `imageUrl` TEXT NOT NULL, `sponsor` TEXT NOT NULL, `clickShim` TEXT NOT NULL, `impressionShim` TEXT NOT NULL, PRIMARY KEY(`url`))",
+ "fields": [
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "imageUrl",
+ "columnName": "imageUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sponsor",
+ "columnName": "sponsor",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "clickShim",
+ "columnName": "clickShim",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "impressionShim",
+ "columnName": "impressionShim",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "url"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1ea41b5cc0791d92dd8f0db8b387fe6c')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/3.json b/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/3.json
new file mode 100644
index 00000000000..967bb2a9c43
--- /dev/null
+++ b/components/service/pocket/schemas/mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase/3.json
@@ -0,0 +1,194 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "966f55824415a21a73640bd2641772f2",
+ "entities": [
+ {
+ "tableName": "stories",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `imageUrl` TEXT NOT NULL, `publisher` TEXT NOT NULL, `category` TEXT NOT NULL, `timeToRead` INTEGER NOT NULL, `timesShown` INTEGER NOT NULL, PRIMARY KEY(`url`))",
+ "fields": [
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "imageUrl",
+ "columnName": "imageUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publisher",
+ "columnName": "publisher",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "category",
+ "columnName": "category",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timeToRead",
+ "columnName": "timeToRead",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timesShown",
+ "columnName": "timesShown",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "url"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "spocs",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `imageUrl` TEXT NOT NULL, `sponsor` TEXT NOT NULL, `clickShim` TEXT NOT NULL, `impressionShim` TEXT NOT NULL, `priority` INTEGER NOT NULL, `lifetimeCapCount` INTEGER NOT NULL, `flightCapCount` INTEGER NOT NULL, `flightCapPeriod` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "imageUrl",
+ "columnName": "imageUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sponsor",
+ "columnName": "sponsor",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "clickShim",
+ "columnName": "clickShim",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "impressionShim",
+ "columnName": "impressionShim",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "priority",
+ "columnName": "priority",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lifetimeCapCount",
+ "columnName": "lifetimeCapCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flightCapCount",
+ "columnName": "flightCapCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flightCapPeriod",
+ "columnName": "flightCapPeriod",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "spocs_impressions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`spocId` INTEGER NOT NULL, `impressionId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `impressionDateInSeconds` INTEGER NOT NULL, FOREIGN KEY(`spocId`) REFERENCES `spocs`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "spocId",
+ "columnName": "spocId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "impressionId",
+ "columnName": "impressionId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "impressionDateInSeconds",
+ "columnName": "impressionDateInSeconds",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "impressionId"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "spocs",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "spocId"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '966f55824415a21a73640bd2641772f2')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/components/service/pocket/src/androidTest/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabaseTest.kt b/components/service/pocket/src/androidTest/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabaseTest.kt
new file mode 100644
index 00000000000..b42924c5d49
--- /dev/null
+++ b/components/service/pocket/src/androidTest/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabaseTest.kt
@@ -0,0 +1,417 @@
+/* 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.service.pocket.stories.db
+
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.room.testing.MigrationTestHelper
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlinx.coroutines.runBlocking
+import mozilla.components.service.pocket.spocs.db.SpocEntity
+import mozilla.components.service.pocket.spocs.db.SpocImpressionEntity
+import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase.Companion
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+private const val MIGRATION_TEST_DB = "migration-test"
+
+class PocketRecommendationsDatabaseTest {
+ private lateinit var context: Context
+ private lateinit var executor: ExecutorService
+ private lateinit var database: PocketRecommendationsDatabase
+
+ @get:Rule
+ @Suppress("DEPRECATION")
+ val helper: MigrationTestHelper = MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ PocketRecommendationsDatabase::class.java.canonicalName,
+ FrameworkSQLiteOpenHelperFactory()
+ )
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Before
+ fun setUp() {
+ executor = Executors.newSingleThreadExecutor()
+
+ context = ApplicationProvider.getApplicationContext()
+ database = Room.inMemoryDatabaseBuilder(context, PocketRecommendationsDatabase::class.java).build()
+ }
+
+ @After
+ fun tearDown() {
+ executor.shutdown()
+ database.close()
+ }
+
+ @Test
+ fun `test1To2MigrationAddsNewSpocsTable`() = runBlocking {
+ // Create the database with the version 1 schema
+ val dbVersion1 = helper.createDatabase(MIGRATION_TEST_DB, 1).apply {
+ execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_STORIES}' " +
+ "(url, title, imageUrl, publisher, category, timeToRead, timesShown) " +
+ "VALUES (" +
+ "'${story.url}'," +
+ "'${story.title}'," +
+ "'${story.imageUrl}'," +
+ "'${story.publisher}'," +
+ "'${story.category}'," +
+ "'${story.timeToRead}'," +
+ "'${story.timesShown}'" +
+ ")"
+ )
+ }
+ // Validate the persisted data which will be re-checked after migration
+ dbVersion1.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_STORIES}"
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ story,
+ PocketStoryEntity(
+ url = cursor.getString(0),
+ title = cursor.getString(1),
+ imageUrl = cursor.getString(2),
+ publisher = cursor.getString(3),
+ category = cursor.getString(4),
+ timeToRead = cursor.getInt(5),
+ timesShown = cursor.getLong(6),
+ )
+ )
+ }
+
+ // Migrate the initial database to the version 2 schema
+ val dbVersion2 = helper.runMigrationsAndValidate(
+ MIGRATION_TEST_DB, 2, true, Migrations.migration_1_2
+ ).apply {
+ execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}' " +
+ "(url, title, imageUrl, sponsor, clickShim, impressionShim) " +
+ "VALUES (" +
+ "'${spoc.url}'," +
+ "'${spoc.title}'," +
+ "'${spoc.imageUrl}'," +
+ "'${spoc.sponsor}'," +
+ "'${spoc.clickShim}'," +
+ "'${spoc.impressionShim}'" +
+ ")"
+ )
+ }
+ // Re-check the initial data we had
+ dbVersion2.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_STORIES}"
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ story,
+ PocketStoryEntity(
+ url = cursor.getString(0),
+ title = cursor.getString(1),
+ imageUrl = cursor.getString(2),
+ publisher = cursor.getString(3),
+ category = cursor.getString(4),
+ timeToRead = cursor.getInt(5),
+ timesShown = cursor.getLong(6),
+ )
+ )
+ }
+ // Finally validate that the new spocs are persisted successfully
+ dbVersion2.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}"
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(spoc.url, cursor.getString(0))
+ assertEquals(spoc.title, cursor.getString(1))
+ assertEquals(spoc.imageUrl, cursor.getString(2))
+ assertEquals(spoc.sponsor, cursor.getString(3))
+ assertEquals(spoc.clickShim, cursor.getString(4))
+ assertEquals(spoc.impressionShim, cursor.getString(5))
+ }
+ }
+
+ @Test
+ fun `test2To3MigrationDropsOldSpocsTableAndAddsNewSpocsAndSpocsImpressionsTables`() = runBlocking {
+ // Create the database with the version 2 schema
+ val dbVersion2 = helper.createDatabase(MIGRATION_TEST_DB, 2).apply {
+ execSQL(
+ "INSERT INTO " +
+ "'${Companion.TABLE_NAME_STORIES}' " +
+ "(url, title, imageUrl, publisher, category, timeToRead, timesShown) " +
+ "VALUES (" +
+ "'${story.url}'," +
+ "'${story.title}'," +
+ "'${story.imageUrl}'," +
+ "'${story.publisher}'," +
+ "'${story.category}'," +
+ "'${story.timeToRead}'," +
+ "'${story.timesShown}'" +
+ ")"
+ )
+ execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}' " +
+ "(url, title, imageUrl, sponsor, clickShim, impressionShim) " +
+ "VALUES (" +
+ "'${spoc.url}'," +
+ "'${spoc.title}'," +
+ "'${spoc.imageUrl}'," +
+ "'${spoc.sponsor}'," +
+ "'${spoc.clickShim}'," +
+ "'${spoc.impressionShim}'" +
+ ")"
+ )
+ }
+
+ // Validate the recommended stories data which will be re-checked after migration
+ dbVersion2.query(
+ "SELECT * FROM ${Companion.TABLE_NAME_STORIES}"
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ story,
+ PocketStoryEntity(
+ url = cursor.getString(0),
+ title = cursor.getString(1),
+ imageUrl = cursor.getString(2),
+ publisher = cursor.getString(3),
+ category = cursor.getString(4),
+ timeToRead = cursor.getInt(5),
+ timesShown = cursor.getLong(6),
+ )
+ )
+ }
+
+ // Migrate to v3 database
+ val dbVersion3 = helper.runMigrationsAndValidate(
+ MIGRATION_TEST_DB, 3, true, Migrations.migration_2_3
+ )
+
+ // Check that recommended stories are unchanged.
+ dbVersion3.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_STORIES}"
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ story,
+ PocketStoryEntity(
+ url = cursor.getString(0),
+ title = cursor.getString(1),
+ imageUrl = cursor.getString(2),
+ publisher = cursor.getString(3),
+ category = cursor.getString(4),
+ timeToRead = cursor.getInt(5),
+ timesShown = cursor.getLong(6),
+ )
+ )
+ }
+
+ // Finally validate that we have two new empty tables for spocs and spocs impressions.
+ dbVersion3.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}"
+ ).use { cursor ->
+ assertEquals(0, cursor.count)
+ assertEquals(11, cursor.columnCount)
+
+ assertEquals("id", cursor.getColumnName(0))
+ assertEquals("url", cursor.getColumnName(1))
+ assertEquals("title", cursor.getColumnName(2))
+ assertEquals("imageUrl", cursor.getColumnName(3))
+ assertEquals("sponsor", cursor.getColumnName(4))
+ assertEquals("clickShim", cursor.getColumnName(5))
+ assertEquals("impressionShim", cursor.getColumnName(6))
+ assertEquals("priority", cursor.getColumnName(7))
+ assertEquals("lifetimeCapCount", cursor.getColumnName(8))
+ assertEquals("flightCapCount", cursor.getColumnName(9))
+ assertEquals("flightCapPeriod", cursor.getColumnName(10))
+ }
+ dbVersion3.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}"
+ ).use { cursor ->
+ assertEquals(0, cursor.count)
+ assertEquals(3, cursor.columnCount)
+
+ assertEquals("spocId", cursor.getColumnName(0))
+ assertEquals("impressionId", cursor.getColumnName(1))
+ assertEquals("impressionDateInSeconds", cursor.getColumnName(2))
+ }
+ }
+
+ @Test
+ fun `test1To3MigrationAddsNewSpocsAndSpocsImpressionsTables`() = runBlocking {
+ // Create the database with the version 1 schema
+ val dbVersion1 = helper.createDatabase(MIGRATION_TEST_DB, 1).apply {
+ execSQL(
+ "INSERT INTO " +
+ "'${Companion.TABLE_NAME_STORIES}' " +
+ "(url, title, imageUrl, publisher, category, timeToRead, timesShown) " +
+ "VALUES (" +
+ "'${story.url}'," +
+ "'${story.title}'," +
+ "'${story.imageUrl}'," +
+ "'${story.publisher}'," +
+ "'${story.category}'," +
+ "'${story.timeToRead}'," +
+ "'${story.timesShown}'" +
+ ")"
+ )
+ }
+ // Validate the persisted data which will be re-checked after migration
+ dbVersion1.query(
+ "SELECT * FROM ${Companion.TABLE_NAME_STORIES}"
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ story,
+ PocketStoryEntity(
+ url = cursor.getString(0),
+ title = cursor.getString(1),
+ imageUrl = cursor.getString(2),
+ publisher = cursor.getString(3),
+ category = cursor.getString(4),
+ timeToRead = cursor.getInt(5),
+ timesShown = cursor.getLong(6),
+ )
+ )
+ }
+
+ val impression = SpocImpressionEntity(spoc.id).apply {
+ impressionId = 1
+ impressionDateInSeconds = 700L
+ }
+ // Migrate the initial database to the version 2 schema
+ val dbVersion3 = helper.runMigrationsAndValidate(
+ MIGRATION_TEST_DB, 3, true, Migrations.migration_1_3
+ ).apply {
+ execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}' (" +
+ "id, url, title, imageUrl, sponsor, clickShim, impressionShim, " +
+ "priority, lifetimeCapCount, flightCapCount, flightCapPeriod" +
+ ") VALUES (" +
+ "'${spoc.id}'," +
+ "'${spoc.url}'," +
+ "'${spoc.title}'," +
+ "'${spoc.imageUrl}'," +
+ "'${spoc.sponsor}'," +
+ "'${spoc.clickShim}'," +
+ "'${spoc.impressionShim}'," +
+ "'${spoc.priority}'," +
+ "'${spoc.lifetimeCapCount}'," +
+ "'${spoc.flightCapCount}'," +
+ "'${spoc.flightCapPeriod}'" +
+ ")"
+ )
+
+ execSQL(
+ "INSERT INTO " +
+ "'${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}' (" +
+ "spocId, impressionId, impressionDateInSeconds" +
+ ") VALUES (" +
+ "'${impression.spocId}'," +
+ "'${impression.impressionId}'," +
+ "'${impression.impressionDateInSeconds}'" +
+ ")"
+ )
+ }
+ // Re-check the initial data we had
+ dbVersion3.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_STORIES}"
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(
+ story,
+ PocketStoryEntity(
+ url = cursor.getString(0),
+ title = cursor.getString(1),
+ imageUrl = cursor.getString(2),
+ publisher = cursor.getString(3),
+ category = cursor.getString(4),
+ timeToRead = cursor.getInt(5),
+ timesShown = cursor.getLong(6),
+ )
+ )
+ }
+ // Finally validate that the new spocs are persisted successfully
+ dbVersion3.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}"
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(spoc.id, cursor.getInt(0))
+ assertEquals(spoc.url, cursor.getString(1))
+ assertEquals(spoc.title, cursor.getString(2))
+ assertEquals(spoc.imageUrl, cursor.getString(3))
+ assertEquals(spoc.sponsor, cursor.getString(4))
+ assertEquals(spoc.clickShim, cursor.getString(5))
+ assertEquals(spoc.impressionShim, cursor.getString(6))
+ assertEquals(spoc.priority, cursor.getInt(7))
+ assertEquals(spoc.lifetimeCapCount, cursor.getInt(8))
+ assertEquals(spoc.flightCapCount, cursor.getInt(9))
+ assertEquals(spoc.flightCapPeriod, cursor.getInt(10))
+ }
+ // And that the impression was also persisted successfully
+ dbVersion3.query(
+ "SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}"
+ ).use { cursor ->
+ assertEquals(1, cursor.count)
+
+ cursor.moveToFirst()
+ assertEquals(impression.spocId, cursor.getInt(0))
+ assertEquals(impression.impressionId, cursor.getInt(1))
+ assertEquals(impression.impressionDateInSeconds, cursor.getLong(2))
+ }
+ }
+}
+
+private val story = PocketStoryEntity(
+ title = "How to Get Rid of Black Mold Naturally",
+ url = "https://getpocket.com/explore/item/how-to-get-rid-of-black-mold-naturally",
+ imageUrl = "https://img-getpocket.cdn.mozilla.net/{wh}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/https%3A%2F%2Fpocket-image-cache.com%2F1200x%2Ffilters%3Aformat(jpg)%3Aextract_focal()%2Fhttps%253A%252F%252Fpocket-syndicated-images.s3.amazonaws.com%252Farticles%252F6757%252F1628024495_6109ae86db6cc.png",
+ publisher = "Pocket",
+ category = "general",
+ timeToRead = 4,
+ timesShown = 23
+)
+
+private val spoc = SpocEntity(
+ id = 191739319,
+ url = "https://i.geistm.com/l/GC_7ReasonsKetoV2_Journiest?bcid=601c567ac5b18a0414cce1d4&bhid=624f3ea9adad7604086ac6b3&utm_content=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off_601c567ac5b18a0414cce1d4_624f3ea9adad7604086ac6b3&tv=su4&ct=NAT-PK-PROS-130OFF5WEEK-037&utm_medium=DB&utm_source=pocket~geistm&utm_campaign=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off",
+ title = "Eating Keto Has Never Been So Easy With Green Chef",
+ imageUrl = "https://img-getpocket.cdn.mozilla.net/direct?url=realUrl.png&resize=w618-h310",
+ sponsor = "Green Chef",
+ clickShim = "193815086ClickShim",
+ impressionShim = "193815086ImpressionShim",
+ priority = 3,
+ lifetimeCapCount = 50,
+ flightCapCount = 10,
+ flightCapPeriod = 86400,
+)
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/GlobalDependencyProvider.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/GlobalDependencyProvider.kt
new file mode 100644
index 00000000000..f05dd7dbe7c
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/GlobalDependencyProvider.kt
@@ -0,0 +1,69 @@
+/* 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.service.pocket
+
+import android.annotation.SuppressLint
+import mozilla.components.service.pocket.spocs.SpocsUseCases
+import mozilla.components.service.pocket.stories.PocketStoriesUseCases
+
+/**
+ * Provides global access to the dependencies needed for updating Pocket stories.
+ */
+internal object GlobalDependencyProvider {
+ internal object RecommendedStories {
+ /**
+ * Possible actions regarding the list of recommended stories.
+ */
+ @SuppressLint("StaticFieldLeak")
+ internal var useCases: PocketStoriesUseCases? = null
+ private set
+
+ /**
+ * Convenience method for setting all details used when communicating with the Pocket server.
+ *
+ * @param useCases [PocketStoriesUseCases] containing all possible actions regarding
+ * the list of recommended stories.
+ */
+ internal fun initialize(
+ useCases: PocketStoriesUseCases,
+ ) {
+ this.useCases = useCases
+ }
+
+ /**
+ * Convenience method for cleaning up any resources held for communicating with the Pocket server.
+ */
+ internal fun reset() {
+ this.useCases = null
+ }
+ }
+
+ internal object SponsoredStories {
+ /**
+ * Possible actions regarding the list of sponsored stories.
+ */
+ @SuppressLint("StaticFieldLeak")
+ internal var useCases: SpocsUseCases? = null
+ private set
+
+ /**
+ * Convenience method for setting all details used when communicating with the Pocket server.
+ *
+ * @param useCases [SpocsUseCases] containing all possible actions regarding the list of sponsored stories.
+ */
+ internal fun initialize(
+ useCases: SpocsUseCases,
+ ) {
+ this.useCases = useCases
+ }
+
+ /**
+ * Convenience method for cleaning up any resources held for communicating with the Pocket server.
+ */
+ internal fun reset() {
+ useCases = null
+ }
+ }
+}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketRecommendedStory.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketRecommendedStory.kt
deleted file mode 100644
index 98e99f3d5af..00000000000
--- a/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketRecommendedStory.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/* 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.service.pocket
-
-/**
- * A Pocket recommended story.
- *
- * @property title the title of the story.
- * @property url a "pocket.co" shortlink for the original story's page.
- * @property imageUrl a url to a still image representing the story.
- * @property publisher optional publisher name/domain, e.g. "The New Yorker" / "nationalgeographic.co.uk"".
- * **May be empty**.
- * @property category topic of interest under which similar stories are grouped.
- * @property timeToRead inferred time needed to read the entire story. **May be -1**.
- */
-data class PocketRecommendedStory(
- val title: String,
- val url: String,
- val imageUrl: String,
- val publisher: String,
- val category: String,
- val timeToRead: Int,
- val timesShown: Long
-)
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesConfig.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesConfig.kt
index 05db3eb329f..3d2403c0961 100644
--- a/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesConfig.kt
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesConfig.kt
@@ -6,22 +6,42 @@ package mozilla.components.service.pocket
import mozilla.components.concept.fetch.Client
import mozilla.components.support.base.worker.Frequency
+import java.util.UUID
import java.util.concurrent.TimeUnit
internal const val DEFAULT_REFRESH_INTERVAL = 4L
+internal const val DEFAULT_SPONSORED_STORIES_REFRESH_INTERVAL = 4L
@Suppress("TopLevelPropertyNaming")
internal val DEFAULT_REFRESH_TIMEUNIT = TimeUnit.HOURS
+@Suppress("TopLevelPropertyNaming")
+internal val DEFAULT_SPONSORED_STORIES_REFRESH_TIMEUNIT = TimeUnit.HOURS
/**
* Indicating all details for how the pocket stories should be refreshed.
*
* @param client [Client] implementation used for downloading the Pocket stories.
- * @param frequency Optional - The interval at which to try and refresh items. Defaults to 1 hour.
+ * @param frequency Optional - The interval at which to try and refresh items. Defaults to 4 hours.
+ * @param profile Optional - The profile used for downloading sponsored Pocket stories.
+ * @param sponsoredStoriesRefreshFrequency Optional - The interval at which to try and refresh sponsored stories.
+ * Defaults to 4 hours.
*/
class PocketStoriesConfig(
val client: Client,
val frequency: Frequency = Frequency(
DEFAULT_REFRESH_INTERVAL,
DEFAULT_REFRESH_TIMEUNIT
+ ),
+ val profile: Profile? = null,
+ val sponsoredStoriesRefreshFrequency: Frequency = Frequency(
+ DEFAULT_SPONSORED_STORIES_REFRESH_INTERVAL,
+ DEFAULT_SPONSORED_STORIES_REFRESH_TIMEUNIT
)
)
+
+/**
+ * Sponsored stories configuration data.
+ *
+ * @param profileId Unique profile identifier which will be presented with sponsored stories.
+ * @param appId Unique identifier of the application using this feature.
+ */
+class Profile(val profileId: UUID, val appId: String)
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesService.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesService.kt
index 14ee30c9a1e..4edd1726603 100644
--- a/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesService.kt
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStoriesService.kt
@@ -5,8 +5,13 @@
package mozilla.components.service.pocket
import android.content.Context
+import androidx.annotation.VisibleForTesting
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
+import mozilla.components.service.pocket.spocs.SpocsUseCases
import mozilla.components.service.pocket.stories.PocketStoriesUseCases
-import mozilla.components.service.pocket.stories.update.PocketStoriesRefreshScheduler
+import mozilla.components.service.pocket.update.PocketStoriesRefreshScheduler
+import mozilla.components.service.pocket.update.SpocsRefreshScheduler
/**
* Allows for getting a list of pocket stories based on the provided [PocketStoriesConfig]
@@ -18,10 +23,28 @@ class PocketStoriesService(
private val context: Context,
private val pocketStoriesConfig: PocketStoriesConfig
) {
- internal var scheduler = PocketStoriesRefreshScheduler(pocketStoriesConfig)
- private val useCases = PocketStoriesUseCases()
- internal var getStoriesUsecase = useCases.GetPocketStories(context)
- internal var updateStoriesTimesShownUsecase = useCases.UpdateStoriesTimesShown(context)
+ @VisibleForTesting
+ internal var storiesRefreshScheduler = PocketStoriesRefreshScheduler(pocketStoriesConfig)
+ @VisibleForTesting
+ internal var spocsRefreshscheduler = SpocsRefreshScheduler(pocketStoriesConfig)
+ @VisibleForTesting
+ internal var storiesUseCases = PocketStoriesUseCases(
+ appContext = context,
+ fetchClient = pocketStoriesConfig.client
+ )
+ @VisibleForTesting
+ internal var spocsUseCases = when (pocketStoriesConfig.profile) {
+ null -> {
+ logger.debug("Missing profile for sponsored stories")
+ null
+ }
+ else -> SpocsUseCases(
+ appContext = context,
+ fetchClient = pocketStoriesConfig.client,
+ profileId = pocketStoriesConfig.profile.profileId,
+ appId = pocketStoriesConfig.profile.appId
+ )
+ }
/**
* Entry point to start fetching Pocket stories in the background.
@@ -33,8 +56,8 @@ class PocketStoriesService(
* making them available for the [getStories] method.
*/
fun startPeriodicStoriesRefresh() {
- PocketStoriesUseCases.initialize(pocketStoriesConfig.client)
- scheduler.schedulePeriodicRefreshes(context)
+ GlobalDependencyProvider.RecommendedStories.initialize(storiesUseCases)
+ storiesRefreshScheduler.schedulePeriodicRefreshes(context)
}
/**
@@ -46,8 +69,8 @@ class PocketStoriesService(
* This stops the process of downloading and caching Pocket stories in the background.
*/
fun stopPeriodicStoriesRefresh() {
- scheduler.stopPeriodicRefreshes(context)
- PocketStoriesUseCases.reset()
+ storiesRefreshScheduler.stopPeriodicRefreshes(context)
+ GlobalDependencyProvider.RecommendedStories.reset()
}
/**
@@ -58,7 +81,63 @@ class PocketStoriesService(
* [startPeriodicStoriesRefresh] hasn't yet completed.
*/
suspend fun getStories(): List {
- return getStoriesUsecase.invoke()
+ return storiesUseCases.getStories()
+ }
+
+ /**
+ * Entry point to start fetching Pocket sponsored stories in the background.
+ *
+ * Use this at an as high as possible level in your application.
+ * Must be paired in a similar way with the [stopPeriodicSponsoredStoriesRefresh] method.
+ *
+ * This starts the process of downloading and caching Pocket sponsored stories in the background,
+ * making them available for the [getSponsoredStories] method.
+ */
+ fun startPeriodicSponsoredStoriesRefresh() {
+ val useCases = spocsUseCases
+ if (useCases == null) {
+ logger.warn("Cannot start sponsored stories refresh. Service has incomplete setup")
+ return
+ }
+
+ GlobalDependencyProvider.SponsoredStories.initialize(useCases)
+ spocsRefreshscheduler.stopProfileDeletion(context)
+ spocsRefreshscheduler.schedulePeriodicRefreshes(context)
+ }
+
+ /**
+ * Single stopping point for the "refresh sponsored Pocket stories" functionality.
+ *
+ * Use this at an as high as possible level in your application.
+ * Must be paired in a similar way with the [startPeriodicSponsoredStoriesRefresh] method.
+ *
+ * This stops the process of downloading and caching Pocket sponsored stories in the background.
+ */
+ fun stopPeriodicSponsoredStoriesRefresh() {
+ spocsRefreshscheduler.stopPeriodicRefreshes(context)
+ }
+
+ /**
+ * Get a list of Pocket sponsored stories based on the initial configuration.
+ */
+ suspend fun getSponsoredStories(): List {
+ return spocsUseCases?.getStories?.invoke() ?: emptyList()
+ }
+
+ /**
+ * Delete all stored user data used for downloading personalized sponsored stories.
+ * This returns immediately but will handle the profile deletion in background.
+ */
+ fun deleteProfile() {
+ val useCases = spocsUseCases
+ if (useCases == null) {
+ logger.warn("Cannot delete sponsored stories profile. Service has incomplete setup")
+ return
+ }
+
+ GlobalDependencyProvider.SponsoredStories.initialize(useCases)
+ spocsRefreshscheduler.stopPeriodicRefreshes(context)
+ spocsRefreshscheduler.scheduleProfileDeletion(context)
}
/**
@@ -68,6 +147,15 @@ class PocketStoriesService(
* Automatically synchronized with the other [PocketStoriesService] methods.
*/
suspend fun updateStoriesTimesShown(updatedStories: List) {
- updateStoriesTimesShownUsecase.invoke(updatedStories)
+ storiesUseCases.updateTimesShown(updatedStories)
+ }
+
+ /**
+ * Persist locally that the sponsored Pocket stories containing the ids from [storiesShown]
+ * were shown to the user.
+ * This is safe to call with any ids, even ones for stories not currently persisted anymore.
+ */
+ suspend fun recordStoriesImpressions(storiesShown: List) {
+ spocsUseCases?.recordImpression?.invoke(storiesShown)
}
}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStory.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStory.kt
new file mode 100644
index 00000000000..a14a9a9e6ca
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/PocketStory.kt
@@ -0,0 +1,99 @@
+/* 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.service.pocket
+
+/**
+ * A Pocket story downloaded from the Internet and intended to be displayed in the application.
+ */
+sealed class PocketStory {
+ /**
+ * Title of the story.
+ */
+ abstract val title: String
+
+ /**
+ * Url where the story can be full read.
+ */
+ abstract val url: String
+
+ /**
+ * A Pocket recommended story.
+ *
+ * @property title The title of the story.
+ * @property url A "pocket.co" shortlink for the original story's page.
+ * @property imageUrl A url to a still image representing the story.
+ * @property publisher Optional publisher name/domain, e.g. "The New Yorker" / "nationalgeographic.co.uk"".
+ * **May be empty**.
+ * @property category Topic of interest under which similar stories are grouped.
+ * @property timeToRead Inferred time needed to read the entire story. **May be -1**.
+ */
+ data class PocketRecommendedStory(
+ override val title: String,
+ override val url: String,
+ val imageUrl: String,
+ val publisher: String,
+ val category: String,
+ val timeToRead: Int,
+ val timesShown: Long
+ ) : PocketStory()
+
+ /**
+ * A Pocket sponsored story.
+ *
+ * @property id Unique id of this story.
+ * @property title The title of the story.
+ * @property url 3rd party url containing the original story.
+ * @property imageUrl A url to a still image representing the story.
+ * Contains a "resize" parameter in the form of "resize=w618-h310" allowing to get the image
+ * with a specific resolution and the CENTER_CROP ScaleType.
+ * @property sponsor 3rd party sponsor of this story, e.g. "NextAdvisor".
+ * @property shim Unique identifiers for when the user interacts with this story.
+ * @property priority Priority level in deciding which stories to be shown first.
+ * A lowest number means a higher priority.
+ * @property caps Story caps indented to control the maximum number of times the story should be shown.
+ */
+ data class PocketSponsoredStory(
+ val id: Int,
+ override val title: String,
+ override val url: String,
+ val imageUrl: String,
+ val sponsor: String,
+ val shim: PocketSponsoredStoryShim,
+ val priority: Int,
+ val caps: PocketSponsoredStoryCaps,
+ ) : PocketStory()
+
+ /**
+ * Sponsored story unique identifiers intended to be used in telemetry.
+ *
+ * @property click Unique identifier for when the sponsored story is clicked.
+ * @property impression Unique identifier for when the user sees this sponsored story.
+ */
+ data class PocketSponsoredStoryShim(
+ val click: String,
+ val impression: String,
+ )
+
+ /**
+ * Sponsored story caps indented to control the maximum number of times the story should be shown.
+ *
+ * @property currentImpressions List of all recorded impression of a sponsored Pocket story
+ * expressed in seconds from Epoch (as the result of `System.currentTimeMillis / 1000`).
+ * @property lifetimeCount Lifetime maximum number of times this story should be shown.
+ * This is independent from the count based on [flightCount] and [flightPeriod] and must never be reset.
+ * @property flightCount Maximum number of times this story should be shown in [flightPeriod].
+ * @property flightPeriod Period expressed as a number of seconds in which this story should be shown
+ * for at most [flightCount] times.
+ * Any time the period comes to an end the [flightCount] count should be restarted.
+ * Even if based on [flightCount] and [flightCount] this story can still be shown a couple more times
+ * if [lifetimeCount] was met then the story should not be shown anymore.
+ */
+ data class PocketSponsoredStoryCaps(
+ val currentImpressions: List = emptyList(),
+ val lifetimeCount: Int,
+ val flightCount: Int,
+ val flightPeriod: Int,
+ )
+}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/api/ext/ConceptFetch.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/ConceptFetch.kt
similarity index 95%
rename from components/service/pocket/src/main/java/mozilla/components/service/pocket/api/ext/ConceptFetch.kt
rename to components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/ConceptFetch.kt
index 412d0cbf4c4..24e4f0871fb 100644
--- a/components/service/pocket/src/main/java/mozilla/components/service/pocket/api/ext/ConceptFetch.kt
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/ConceptFetch.kt
@@ -2,7 +2,7 @@
* 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.service.pocket.api.ext
+package mozilla.components.service.pocket.ext
import androidx.annotation.WorkerThread
import mozilla.components.concept.fetch.Client
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/Mappers.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/Mappers.kt
new file mode 100644
index 00000000000..f3c8527be78
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/Mappers.kt
@@ -0,0 +1,92 @@
+/* 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.service.pocket.ext
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryCaps
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryShim
+import mozilla.components.service.pocket.spocs.api.ApiSpoc
+import mozilla.components.service.pocket.spocs.db.SpocEntity
+import mozilla.components.service.pocket.stories.api.PocketApiStory
+import mozilla.components.service.pocket.stories.db.PocketLocalStoryTimesShown
+import mozilla.components.service.pocket.stories.db.PocketStoryEntity
+
+@VisibleForTesting
+internal const val DEFAULT_CATEGORY = "general"
+@VisibleForTesting
+internal const val DEFAULT_TIMES_SHOWN = 0L
+
+/**
+ * Map Pocket API objects to the object type that we persist locally.
+ */
+internal fun PocketApiStory.toPocketLocalStory(): PocketStoryEntity =
+ PocketStoryEntity(
+ url, title, imageUrl, publisher, category, timeToRead, DEFAULT_TIMES_SHOWN
+ )
+
+/**
+ * Map Room entities to the object type that we expose to service clients.
+ */
+internal fun PocketStoryEntity.toPocketRecommendedStory(): PocketRecommendedStory =
+ PocketRecommendedStory(
+ url = url,
+ title = title,
+ imageUrl = imageUrl,
+ publisher = publisher,
+ category = if (category.isNotBlank()) category else DEFAULT_CATEGORY,
+ timeToRead = timeToRead,
+ timesShown = timesShown
+ )
+
+/**
+ * Maps an object of the type exposed to clients to one that can partially update only the "timesShown"
+ * property of the type we persist locally.
+ */
+internal fun PocketRecommendedStory.toPartialTimeShownUpdate(): PocketLocalStoryTimesShown =
+ PocketLocalStoryTimesShown(url, timesShown)
+
+/**
+ * Map sponsored Pocket stories to the object type that we persist locally.
+ */
+internal fun ApiSpoc.toLocalSpoc(): SpocEntity =
+ SpocEntity(
+ id = flightId,
+ url = url,
+ title = title,
+ imageUrl = imageSrc,
+ sponsor = sponsor,
+ clickShim = shim.click,
+ impressionShim = shim.impression,
+ priority = priority,
+ lifetimeCapCount = caps.lifetimeCount,
+ flightCapCount = caps.flightCount,
+ flightCapPeriod = caps.flightPeriod,
+ )
+
+/**
+ * Map Room entities to the object type that we expose to service clients.
+ */
+internal fun SpocEntity.toPocketSponsoredStory(
+ impressions: List = emptyList()
+) = PocketSponsoredStory(
+ id = id,
+ title = title,
+ url = url,
+ imageUrl = imageUrl,
+ sponsor = sponsor,
+ shim = PocketSponsoredStoryShim(
+ click = clickShim,
+ impression = impressionShim
+ ),
+ priority = priority,
+ caps = PocketSponsoredStoryCaps(
+ currentImpressions = impressions,
+ lifetimeCount = lifetimeCapCount,
+ flightCount = flightCapCount,
+ flightPeriod = flightCapPeriod,
+ ),
+)
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/PocketStory.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/PocketStory.kt
new file mode 100644
index 00000000000..06695e74fcc
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/ext/PocketStory.kt
@@ -0,0 +1,50 @@
+/* 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.service.pocket.ext
+
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryCaps
+import java.util.concurrent.TimeUnit
+
+/**
+ * Get a list of all story impressions (expressed in seconds from Epoch) in the period between
+ * `now` down to [PocketSponsoredStoryCaps.flightPeriod].
+ */
+fun PocketSponsoredStory.getCurrentFlightImpressions(): List {
+ val now = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
+ return caps.currentImpressions.filter {
+ now - it < caps.flightPeriod
+ }
+}
+
+/**
+ * Get if this story was already shown for the maximum number of times available in it's lifetime.
+ */
+fun PocketSponsoredStory.hasLifetimeImpressionsLimitReached(): Boolean {
+ return caps.currentImpressions.size >= caps.lifetimeCount
+}
+
+/**
+ * Get if this story was already shown for the maximum number of times available in the period
+ * specified by [PocketSponsoredStoryCaps.flightPeriod].
+ */
+fun PocketSponsoredStory.hasFlightImpressionsLimitReached(): Boolean {
+ return getCurrentFlightImpressions().size >= caps.flightCount
+}
+
+/**
+ * Record a new impression at this instant time and get this story back with updated impressions details.
+ * This only updates the in-memory data.
+ *
+ * It's recommended to use this method anytime a new impression needs to be recorded for a `PocketSponsoredStory`
+ * to ensure values consistency.
+ */
+fun PocketSponsoredStory.recordNewImpression(): PocketSponsoredStory {
+ return this.copy(
+ caps = caps.copy(
+ currentImpressions = caps.currentImpressions + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
+ )
+ )
+}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsRepository.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsRepository.kt
new file mode 100644
index 00000000000..0425228a1c4
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsRepository.kt
@@ -0,0 +1,68 @@
+/* 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.service.pocket.spocs
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
+import mozilla.components.service.pocket.ext.toLocalSpoc
+import mozilla.components.service.pocket.ext.toPocketSponsoredStory
+import mozilla.components.service.pocket.spocs.api.ApiSpoc
+import mozilla.components.service.pocket.spocs.db.SpocImpressionEntity
+import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase
+
+/**
+ * Wrapper over our local database containing Spocs.
+ * Allows for easy CRUD operations.
+ */
+internal class SpocsRepository(context: Context) {
+ private val database: Lazy = lazy { PocketRecommendationsDatabase.get(context) }
+ @VisibleForTesting
+ internal val spocsDao by lazy { database.value.spocsDao() }
+
+ /**
+ * Get the current locally persisted list of sponsored Pocket stories
+ * complete with the list of all locally persisted impressions data.
+ */
+ suspend fun getAllSpocs(): List {
+ val spocs = spocsDao.getAllSpocs()
+ val impressions = spocsDao.getSpocsImpressions().groupBy { it.spocId }
+
+ return spocs.map { spoc ->
+ spoc.toPocketSponsoredStory(
+ impressions[spoc.id]
+ ?.map { impression -> impression.impressionDateInSeconds }
+ ?: emptyList()
+ )
+ }
+ }
+
+ /**
+ * Delete all currently persisted sponsored Pocket stories.
+ */
+ suspend fun deleteAllSpocs() {
+ spocsDao.deleteAllSpocs()
+ }
+
+ /**
+ * Replace the current list of locally persisted sponsored Pocket stories.
+ *
+ * @param spocs The list of sponsored Pocket stories to persist locally.
+ */
+ suspend fun addSpocs(spocs: List) {
+ spocsDao.cleanOldAndInsertNewSpocs(spocs.map { it.toLocalSpoc() })
+ }
+
+ /**
+ * Add a new impression record for each of the spocs identified by the ids from [spocsShown].
+ * Will ignore adding new entries if the intended spocs are not persisted locally anymore.
+ * Recorded entries will automatically be cleaned when the spoc they target is deleted.
+ *
+ * @param spocsShown List of [PocketSponsoredStory.id] for which to record new impressions.
+ */
+ suspend fun recordImpressions(spocsShown: List) {
+ spocsDao.recordImpressions(spocsShown.map { SpocImpressionEntity(it) })
+ }
+}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsUseCases.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsUseCases.kt
new file mode 100644
index 00000000000..0bc2b7485a1
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/SpocsUseCases.kt
@@ -0,0 +1,172 @@
+/* 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.service.pocket.spocs
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import mozilla.components.concept.fetch.Client
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
+import mozilla.components.service.pocket.spocs.api.SpocsEndpoint
+import mozilla.components.service.pocket.stories.api.PocketResponse.Failure
+import mozilla.components.service.pocket.stories.api.PocketResponse.Success
+import java.util.UUID
+
+/**
+ * Possible actions regarding the list of sponsored stories.
+ *
+ * @param appContext Android Context. Prefer sending application context to limit the possibility of even small leaks.
+ * @param fetchClient the HTTP client to use for network requests.
+ * @param profileId Unique profile identifier used for downloading sponsored Pocket stories.
+ * @param appId Unique app identifier used for downloading sponsored Pocket stories.
+ */
+internal class SpocsUseCases(
+ private val appContext: Context,
+ private val fetchClient: Client,
+ private val profileId: UUID,
+ private val appId: String,
+) {
+ /**
+ * Download and persist an updated list of sponsored stories.
+ */
+ internal val refreshStories by lazy {
+ RefreshSponsoredStories(appContext, fetchClient, profileId, appId)
+ }
+
+ /**
+ * Get the list of available Pocket sponsored stories.
+ */
+ internal val getStories by lazy {
+ GetSponsoredStories(appContext)
+ }
+
+ internal val recordImpression by lazy {
+ RecordImpression(appContext)
+ }
+
+ /**
+ * Delete all stored user data used for downloading sponsored stories.
+ */
+ internal val deleteProfile by lazy {
+ DeleteProfile(appContext, fetchClient, profileId, appId)
+ }
+
+ /**
+ * Allows for refreshing the list of Pocket sponsored stories we have cached.
+ *
+ * @param appContext Android Context. Prefer sending application context to limit the possibility
+ * of even small leaks.
+ * @param fetchClient the HTTP client to use for network requests.
+ * @param profileId Unique profile identifier when using this feature.
+ * @param appId Unique identifier of the application using this feature.
+ */
+ internal inner class RefreshSponsoredStories(
+ @get:VisibleForTesting
+ internal val appContext: Context = this@SpocsUseCases.appContext,
+ @get:VisibleForTesting
+ internal val fetchClient: Client = this@SpocsUseCases.fetchClient,
+ @get:VisibleForTesting
+ internal val profileId: UUID = this@SpocsUseCases.profileId,
+ @get:VisibleForTesting
+ internal val appId: String = this@SpocsUseCases.appId,
+ ) {
+ /**
+ * Do a full download from Pocket -> persist locally cycle for sponsored stories.
+ */
+ suspend operator fun invoke(): Boolean {
+ val provider = getSpocsProvider(fetchClient, profileId, appId)
+ val response = provider.getSponsoredStories()
+
+ if (response is Success) {
+ getSpocsRepository(appContext).addSpocs(response.data)
+ return true
+ }
+
+ return false
+ }
+ }
+
+ /**
+ * Allows for querying the list of available Pocket sponsored stories.
+ *
+ * @param context [Context] used for various system interactions and libraries initializations.
+
+ */
+ internal inner class GetSponsoredStories(
+ @get:VisibleForTesting
+ internal val context: Context = this@SpocsUseCases.appContext,
+ ) {
+ /**
+ * Do an internet query for a list of Pocket sponsored stories.
+ */
+ suspend operator fun invoke(): List {
+ return getSpocsRepository(context).getAllSpocs()
+ }
+ }
+
+ /**
+ * Allows for atomically updating the [PocketRecommendedStory.timesShown] property of some recommended stories.
+ *
+ * @param context [Context] used for various system interactions and libraries initializations.
+ */
+ internal inner class RecordImpression(
+ @get:VisibleForTesting
+ internal val context: Context = this@SpocsUseCases.appContext
+ ) {
+ /**
+ * Update how many times certain stories were shown to the user.
+ */
+ suspend operator fun invoke(storiesShown: List) {
+ if (storiesShown.isNotEmpty()) {
+ getSpocsRepository(context).recordImpressions(storiesShown)
+ }
+ }
+ }
+
+ /**
+ * Allows deleting all stored user data used for downloading sponsored stories.
+ *
+ * @param context [Context] used for various system interactions and libraries initializations.
+ * @param fetchClient the HTTP client to use for network requests.
+ * @param profileId Unique profile identifier previously used for downloading sponsored Pocket stories.
+ * @param appId Unique app identifier previously used for downloading sponsored Pocket stories.
+ */
+ internal inner class DeleteProfile(
+ @get:VisibleForTesting
+ internal val context: Context = this@SpocsUseCases.appContext,
+ @get:VisibleForTesting
+ internal val fetchClient: Client = this@SpocsUseCases.fetchClient,
+ @get:VisibleForTesting
+ internal val profileId: UUID = this@SpocsUseCases.profileId,
+ @get:VisibleForTesting
+ internal val appId: String = this@SpocsUseCases.appId,
+ ) {
+ /**
+ * Delete all stored user data used for downloading personalized sponsored stories.
+ */
+ suspend operator fun invoke(): Boolean {
+ val provider = getSpocsProvider(fetchClient, profileId, appId)
+ return when (provider.deleteProfile()) {
+ is Success -> {
+ getSpocsRepository(context).deleteAllSpocs()
+ true
+ }
+ is Failure -> {
+ // Don't attempt to delete locally persisted stories to prevent mismatching issues
+ // with profile deletion failing - applications still "showing it" but
+ // with no sponsored articles to show.
+ false
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun getSpocsRepository(context: Context) = SpocsRepository(context)
+
+ @VisibleForTesting
+ internal fun getSpocsProvider(client: Client, profileId: UUID, appId: String) =
+ SpocsEndpoint.newInstance(client, profileId, appId)
+}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/ApiSpoc.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/ApiSpoc.kt
new file mode 100644
index 00000000000..bae665bc253
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/ApiSpoc.kt
@@ -0,0 +1,60 @@
+/* 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.service.pocket.spocs.api
+
+/**
+ * A Pocket sponsored as downloaded from the sponsored stories endpoint.
+ *
+ * @property flightId Unique id of this story.
+ * @property title the title of the story.
+ * @property url 3rd party url containing the original story.
+ * @property imageSrc a url to a still image representing the story.
+ * Contains a "resize" parameter in the form of "resize=w618-h310" allowing to get the image
+ * with a specific resolution and the CENTER_CROP ScaleType.
+ * @property sponsor 3rd party sponsor of this story, e.g. "NextAdvisor".
+ * @property shim Unique identifiers for when the user interacts with this story.
+ * @property priority Priority level in deciding which stories to be shown first.
+ * A lowest number means a higher priority.
+ * @property caps Story caps indented to control the maximum number of times the story should be shown.
+ */
+internal data class ApiSpoc(
+ val flightId: Int,
+ val title: String,
+ val url: String,
+ val imageSrc: String,
+ val sponsor: String,
+ val shim: ApiSpocShim,
+ val priority: Int,
+ val caps: ApiSpocCaps,
+)
+
+/**
+ * Sponsored story unique identifiers intended to be used in telemetry.
+ *
+ * @property click Unique identifier for when the sponsored story is clicked.
+ * @property impression Unique identifier for when the user sees this sponsored story.
+ */
+internal data class ApiSpocShim(
+ val click: String,
+ val impression: String,
+)
+
+/**
+ * Sponsored story caps indented to control the maximum number of times the story should be shown.
+ *
+ * @property lifetimeCount Lifetime maximum number of times this story should be shown.
+ * This is independent from the count based on [flightCount] and [flightPeriod] and must never be reset.
+ * @property flightCount Maximum number of times this story should be shown in [flightPeriod].
+ * @property flightPeriod Period expressed as a number of seconds in which this story should be shown
+ * for at most [flightCount] times.
+ * Any time the period comes to an end the [flightCount] count should be restarted.
+ * Even if based on [flightCount] and [flightCount] this story can still be shown a couple more times
+ * if [lifetimeCount] was met then the story should not be shown anymore.
+ */
+internal data class ApiSpocCaps(
+ val lifetimeCount: Int,
+ val flightCount: Int,
+ val flightPeriod: Int,
+)
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpoint.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpoint.kt
new file mode 100644
index 00000000000..9cc8d6fc7fe
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpoint.kt
@@ -0,0 +1,60 @@
+/* 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.service.pocket.spocs.api
+
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import mozilla.components.concept.fetch.Client
+import mozilla.components.service.pocket.spocs.api.SpocsEndpoint.Companion.newInstance
+import mozilla.components.service.pocket.stories.api.PocketEndpoint.Companion.newInstance
+import mozilla.components.service.pocket.stories.api.PocketResponse
+import java.util.UUID
+
+/**
+ * Makes requests to the sponsored stories API and returns the requested data.
+ *
+ * @see [newInstance] to retrieve an instance.
+ */
+internal class SpocsEndpoint internal constructor(
+ @get:VisibleForTesting internal val rawEndpoint: SpocsEndpointRaw,
+ private val jsonParser: SpocsJSONParser
+) : SpocsProvider {
+
+ /**
+ * Download a new list of sponsored Pocket stories.
+ *
+ * If the API returns unexpectedly formatted results, these entries will be omitted and the rest of the items are
+ * returned.
+ *
+ * @return a [PocketResponse.Success] with the sponsored Pocket stories (list may be empty)
+ * or [PocketResponse.Failure] if the request didn't complete successfully.
+ */
+ @WorkerThread
+ override suspend fun getSponsoredStories(): PocketResponse> {
+ val response = rawEndpoint.getSponsoredStories()
+ val spocs = if (response.isNullOrBlank()) null else jsonParser.jsonToSpocs(response)
+ return PocketResponse.wrap(spocs)
+ }
+
+ @WorkerThread
+ override suspend fun deleteProfile(): PocketResponse {
+ val response = rawEndpoint.deleteProfile()
+ return PocketResponse.wrap(response)
+ }
+
+ companion object {
+ /**
+ * Returns a new instance of [SpocsEndpoint].
+ *
+ * @param client the HTTP client to use for network requests.
+ * @param profileId Unique profile identifier which will be presented with sponsored stories.
+ * @param appId Unique identifier of the application using this feature.
+ */
+ fun newInstance(client: Client, profileId: UUID, appId: String): SpocsEndpoint {
+ val rawEndpoint = SpocsEndpointRaw.newInstance(client, profileId, appId)
+ return SpocsEndpoint(rawEndpoint, SpocsJSONParser)
+ }
+ }
+}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRaw.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRaw.kt
new file mode 100644
index 00000000000..b9587004329
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRaw.kt
@@ -0,0 +1,118 @@
+/* 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.service.pocket.spocs.api
+
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.MutableHeaders
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Request.Body
+import mozilla.components.concept.fetch.Request.Method
+import mozilla.components.concept.fetch.Response
+import mozilla.components.concept.fetch.isSuccess
+import mozilla.components.service.pocket.ext.fetchBodyOrNull
+import mozilla.components.service.pocket.logger
+import mozilla.components.service.pocket.spocs.api.SpocsEndpointRaw.Companion.newInstance
+import mozilla.components.service.pocket.stories.api.PocketEndpointRaw.Companion.newInstance
+import org.json.JSONObject
+import java.io.IOException
+import java.util.UUID
+
+private const val SPOCS_ENDPOINT_BASE_URL = "https://spocs.getpocket.dev/"
+private const val SPOCS_ENDPOINT_DOWNLOAD_SPOCS_PATH = "spocs"
+private const val SPOCS_ENDPOINT_DELETE_PROFILE_PATH = "user"
+private const val SPOCS_PROXY_VERSION_KEY = "version"
+private const val SPOCS_PROXY_VERSION_VALUE = "2"
+private const val SPOCS_PROXY_PROFILE_KEY = "pocket_id"
+private const val SPOCS_PROXY_APP_KEY = "consumer_key"
+
+/**
+ * Makes requests to the Pocket endpoint and returns the raw JSON data.
+ *
+ * @see [SpocsEndpoint], which wraps this to make it more practical.
+ * @see [newInstance] to retrieve an instance.
+ */
+internal class SpocsEndpointRaw internal constructor(
+ @get:VisibleForTesting internal val client: Client,
+ @get:VisibleForTesting internal val profileId: UUID,
+ @get:VisibleForTesting internal val appId: String
+) {
+ /**
+ * Gets the current sponsored stories recommendations from the Pocket server.
+ *
+ * @return The stories recommendations as a raw JSON string or null on error.
+ */
+ @WorkerThread
+ fun getSponsoredStories(): String? {
+ val request = Request(
+ url = SPOCS_ENDPOINT_BASE_URL + SPOCS_ENDPOINT_DOWNLOAD_SPOCS_PATH,
+ method = Method.POST,
+ headers = getRequestHeaders(),
+ body = getDownloadStoriesRequestBody()
+ )
+ return client.fetchBodyOrNull(request)
+ }
+
+ /**
+ * Request to delete all data stored on server about [profileId].
+ *
+ * @return [Boolean] indicating whether the delete operation was successful or not.
+ */
+ @WorkerThread
+ fun deleteProfile(): Boolean {
+ val request = Request(
+ url = SPOCS_ENDPOINT_BASE_URL + SPOCS_ENDPOINT_DELETE_PROFILE_PATH,
+ method = Method.DELETE,
+ headers = getRequestHeaders(),
+ body = getDeleteProfileRequestBody()
+ )
+
+ val response: Response? = try {
+ client.fetch(request)
+ } catch (e: IOException) {
+ logger.debug("Network error", e)
+ null
+ }
+
+ return response?.isSuccess ?: false
+ }
+
+ private fun getRequestHeaders() = MutableHeaders(
+ "Content-Type" to "application/json; charset=UTF-8",
+ "Accept" to "*/*"
+ )
+
+ private fun getDownloadStoriesRequestBody(): Body {
+ val params = mapOf(
+ SPOCS_PROXY_VERSION_KEY to SPOCS_PROXY_VERSION_VALUE,
+ SPOCS_PROXY_PROFILE_KEY to profileId.toString(),
+ SPOCS_PROXY_APP_KEY to appId,
+ )
+
+ return Body(JSONObject(params).toString().byteInputStream())
+ }
+
+ private fun getDeleteProfileRequestBody(): Body {
+ val params = mapOf(
+ SPOCS_PROXY_PROFILE_KEY to profileId.toString(),
+ )
+
+ return Body(JSONObject(params).toString().byteInputStream())
+ }
+
+ companion object {
+ /**
+ * Returns a new instance of [SpocsEndpointRaw].
+ *
+ * @param client HTTP client to use for network requests.
+ * @param profileId Unique profile identifier which will be presented with sponsored stories.
+ * @param appId Unique identifier of the application using this feature.
+ */
+ fun newInstance(client: Client, profileId: UUID, appId: String): SpocsEndpointRaw {
+ return SpocsEndpointRaw(client, profileId, appId)
+ }
+ }
+}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParser.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParser.kt
new file mode 100644
index 00000000000..775e5037ddf
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParser.kt
@@ -0,0 +1,85 @@
+/* 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.service.pocket.spocs.api
+
+import androidx.annotation.VisibleForTesting
+import mozilla.components.service.pocket.logger
+import mozilla.components.support.ktx.android.org.json.mapNotNull
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+
+@VisibleForTesting
+internal const val KEY_ARRAY_SPOCS = "spocs"
+@VisibleForTesting
+internal const val JSON_SPOC_SHIMS_KEY = "shim"
+@VisibleForTesting
+internal const val JSON_SPOC_CAPS_KEY = "caps"
+@VisibleForTesting
+internal const val JSON_SPOC_CAPS_LIFETIME_KEY = "lifetime"
+@VisibleForTesting
+internal const val JSON_SPOC_CAPS_FLIGHT_KEY = "campaign"
+@VisibleForTesting
+internal const val JSON_SPOC_CAPS_FLIGHT_COUNT_KEY = "count"
+@VisibleForTesting
+internal const val JSON_SPOC_CAPS_FLIGHT_PERIOD_KEY = "period"
+private const val JSON_SPOC_FLIGHT_ID_KEY = "flight_id"
+private const val JSON_SPOC_TITLE_KEY = "title"
+private const val JSON_SPOC_SPONSOR_KEY = "sponsor"
+private const val JSON_SPOC_URL_KEY = "url"
+private const val JSON_SPOC_IMAGE_SRC_KEY = "image_src"
+private const val JSON_SPOC_SHIM_CLICK_KEY = "click"
+private const val JSON_SPOC_SHIM_IMPRESSION_KEY = "impression"
+private const val JSON_SPOC_PRIORITY = "priority"
+
+/**
+ * Holds functions that parse the JSON returned by the Pocket API and converts them to more usable Kotlin types.
+ */
+internal object SpocsJSONParser {
+ /**
+ * @return The stories, removing entries that are invalid, or null on error; the list will never be empty.
+ */
+ fun jsonToSpocs(json: String): List? = try {
+ val rawJSON = JSONObject(json)
+ val spocsJSON = rawJSON.getJSONArray(KEY_ARRAY_SPOCS)
+ val spocs = spocsJSON.mapNotNull(JSONArray::getJSONObject) { jsonToSpoc(it) }
+
+ // We return null, rather than the empty list, because devs might forget to check an empty list.
+ spocs.ifEmpty { null }
+ } catch (e: JSONException) {
+ logger.warn("invalid JSON from the SPOCS endpoint", e)
+ null
+ }
+
+ private fun jsonToSpoc(json: JSONObject): ApiSpoc? = try {
+ ApiSpoc(
+ flightId = json.getInt(JSON_SPOC_FLIGHT_ID_KEY),
+ title = json.getString(JSON_SPOC_TITLE_KEY),
+ sponsor = json.getString(JSON_SPOC_SPONSOR_KEY),
+ url = json.getString(JSON_SPOC_URL_KEY),
+ imageSrc = json.getString(JSON_SPOC_IMAGE_SRC_KEY),
+ shim = jsonToShim(json.getJSONObject(JSON_SPOC_SHIMS_KEY)),
+ priority = json.getInt(JSON_SPOC_PRIORITY),
+ caps = jsonToCaps(json.getJSONObject(JSON_SPOC_CAPS_KEY)),
+ )
+ } catch (e: JSONException) {
+ null
+ }
+
+ private fun jsonToShim(json: JSONObject) = ApiSpocShim(
+ click = json.getString(JSON_SPOC_SHIM_CLICK_KEY),
+ impression = json.getString(JSON_SPOC_SHIM_IMPRESSION_KEY)
+ )
+
+ private fun jsonToCaps(json: JSONObject): ApiSpocCaps {
+ val flightCaps = json.getJSONObject(JSON_SPOC_CAPS_FLIGHT_KEY)
+
+ return ApiSpocCaps(
+ lifetimeCount = json.getInt(JSON_SPOC_CAPS_LIFETIME_KEY),
+ flightCount = flightCaps.getInt(JSON_SPOC_CAPS_FLIGHT_COUNT_KEY),
+ flightPeriod = flightCaps.getInt(JSON_SPOC_CAPS_FLIGHT_PERIOD_KEY)
+ )
+ }
+}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsProvider.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsProvider.kt
new file mode 100644
index 00000000000..dcb5819cd97
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/api/SpocsProvider.kt
@@ -0,0 +1,27 @@
+/* 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.service.pocket.spocs.api
+
+import mozilla.components.service.pocket.stories.api.PocketResponse
+
+/**
+ * All possible operations related to SPocs - Sponsored Pocket stories.
+ */
+internal interface SpocsProvider {
+ /**
+ * Download new sponsored stories.
+ *
+ * @return [PocketResponse.Success] containing a list of sponsored stories or
+ * [PocketResponse.Failure] if the request didn't complete successfully.
+ */
+ suspend fun getSponsoredStories(): PocketResponse>
+
+ /**
+ * Delete all data associated with [profileId].
+ *
+ * @return [PocketResponse.Success] if the request completed successfully, [PocketResponse.Failure] otherwise.
+ */
+ suspend fun deleteProfile(): PocketResponse
+}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocEntity.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocEntity.kt
new file mode 100644
index 00000000000..02c68b78458
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocEntity.kt
@@ -0,0 +1,41 @@
+/* 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.service.pocket.spocs.db
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase
+
+/**
+ * A sponsored Pocket story that is to be mapped to SQLite table.
+ *
+ * @property id Unique story id serving as the primary key of this entity.
+ * @property url URL where the original story can be read.
+ * @property title Title of the story.
+ * @property imageUrl URL of the hero image for this story.
+ * @property sponsor 3rd party sponsor of this story, e.g. "NextAdvisor".
+ * @property clickShim Telemetry identifier for when the sponsored story is clicked.
+ * @property impressionShim Telemetry identifier for when the sponsored story is seen by the user.
+ * @property priority Priority level in deciding which stories to be shown first.
+ * @property lifetimeCapCount Indicates how many times a sponsored story can be shown in total.
+ * @property flightCapCount Indicates how many times a sponsored story can be shown within a period.
+ * @property flightCapPeriod Indicates the period (number of seconds) in which at most [flightCapCount]
+ * stories can be shown.
+ */
+@Entity(tableName = PocketRecommendationsDatabase.TABLE_NAME_SPOCS)
+internal data class SpocEntity(
+ @PrimaryKey
+ val id: Int,
+ val url: String,
+ val title: String,
+ val imageUrl: String,
+ val sponsor: String,
+ val clickShim: String,
+ val impressionShim: String,
+ val priority: Int,
+ val lifetimeCapCount: Int,
+ val flightCapCount: Int,
+ val flightCapPeriod: Int,
+)
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntity.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntity.kt
new file mode 100644
index 00000000000..9d0f63a2a40
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntity.kt
@@ -0,0 +1,41 @@
+/* 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.service.pocket.spocs.db
+
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.PrimaryKey
+import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase
+
+/**
+ * One sponsored Pocket story impression.
+ * Allows to easily create a relation between a particular spoc identified by it's [SpocEntity.id]
+ * and any number of impressions.
+ *
+ * @property spocId [SpocEntity.id] that this serves as an impression of.
+ * Used as a foreign key allowing to only add impressions for other persisted spocs and
+ * automatically remove all impressions when the spoc they refer to is deleted.
+ * @property impressionId Unique id of this entity. Primary key.
+ * @property impressionDateInSeconds Epoch based timestamp expressed in seconds (from System.currentTimeMillis / 1000)
+ * for when the spoc identified by [spocId] was shown to the user.
+ */
+@Entity(
+ tableName = PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS,
+ foreignKeys = [
+ ForeignKey(
+ entity = SpocEntity::class,
+ parentColumns = arrayOf("id"),
+ childColumns = arrayOf("spocId"),
+ onDelete = ForeignKey.CASCADE,
+ )
+ ]
+)
+internal data class SpocImpressionEntity(
+ val spocId: Int,
+) {
+ @PrimaryKey(autoGenerate = true)
+ var impressionId: Int = 0
+ var impressionDateInSeconds: Long = System.currentTimeMillis() / 1000
+}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocsDao.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocsDao.kt
new file mode 100644
index 00000000000..c74ddb00438
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/spocs/db/SpocsDao.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.service.pocket.spocs.db
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Transaction
+import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase
+import java.util.concurrent.TimeUnit
+
+@Dao
+internal interface SpocsDao {
+ @Transaction
+ suspend fun cleanOldAndInsertNewSpocs(spocs: List) {
+ val newSpocs = spocs.map { it.id }
+ val oldStoriesToDelete = getAllSpocs()
+ .filterNot { newSpocs.contains(it.id) }
+
+ deleteSpocs(oldStoriesToDelete)
+ insertSpocs(spocs)
+ }
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE) // Maybe some details changed
+ suspend fun insertSpocs(stories: List)
+
+ @Transaction
+ suspend fun recordImpressions(stories: List) {
+ stories.forEach {
+ recordImpression(it.spocId, it.impressionDateInSeconds)
+ }
+ }
+
+ /**
+ * INSERT OR IGNORE method needed to prevent against "FOREIGN KEY constraint failed" exceptions
+ * if clients try to insert new impressions spocs not existing anymore in the database in cases where
+ * a different list of spocs were downloaded but the client operates with stale in-memory data.
+ *
+ * @param targetSpocId The `id` of the [SpocEntity] to add a new impression for.
+ * A new impression will be persisted only if a story with the indicated [targetSpocId] currently exists.
+ * @param targetImpressionDateInSeconds The timestamp expressed in seconds from Epoch for this impression.
+ * Defaults to the current time expressed in seconds as get from `System.currentTimeMillis / 1000`.
+ */
+ @Query(
+ "WITH newImpression(spocId, impressionDateInSeconds) AS (VALUES" +
+ "(:targetSpocId, :targetImpressionDateInSeconds)" +
+ ")" +
+ "INSERT INTO spocs_impressions(spocId, impressionDateInSeconds) " +
+ "SELECT impression.spocId, impression.impressionDateInSeconds " +
+ "FROM newImpression impression " +
+ "WHERE EXISTS (SELECT 1 FROM spocs spoc WHERE spoc.id = impression.spocId)"
+ )
+ suspend fun recordImpression(
+ targetSpocId: Int,
+ targetImpressionDateInSeconds: Long = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
+ )
+
+ @Query("DELETE FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}")
+ suspend fun deleteAllSpocs()
+
+ @Delete
+ suspend fun deleteSpocs(stories: List)
+
+ @Query("SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}")
+ suspend fun getAllSpocs(): List
+
+ @Query("SELECT * FROM ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}")
+ suspend fun getSpocsImpressions(): List
+}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepository.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepository.kt
index deb02997781..1589415702a 100644
--- a/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepository.kt
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepository.kt
@@ -6,12 +6,12 @@ package mozilla.components.service.pocket.stories
import android.content.Context
import androidx.annotation.VisibleForTesting
-import mozilla.components.service.pocket.PocketRecommendedStory
-import mozilla.components.service.pocket.api.PocketApiStory
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.ext.toPartialTimeShownUpdate
+import mozilla.components.service.pocket.ext.toPocketLocalStory
+import mozilla.components.service.pocket.ext.toPocketRecommendedStory
+import mozilla.components.service.pocket.stories.api.PocketApiStory
import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase
-import mozilla.components.service.pocket.stories.ext.toPartialTimeShownUpdate
-import mozilla.components.service.pocket.stories.ext.toPocketLocalStory
-import mozilla.components.service.pocket.stories.ext.toPocketRecommendedStory
/**
* Wrapper over our local database.
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketStoriesUseCases.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketStoriesUseCases.kt
index 285b1f906f4..77207b8741f 100644
--- a/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketStoriesUseCases.kt
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/PocketStoriesUseCases.kt
@@ -7,40 +7,57 @@ package mozilla.components.service.pocket.stories
import android.content.Context
import androidx.annotation.VisibleForTesting
import mozilla.components.concept.fetch.Client
-import mozilla.components.service.pocket.PocketRecommendedStory
-import mozilla.components.service.pocket.api.PocketEndpoint
-import mozilla.components.service.pocket.api.PocketResponse
-import mozilla.components.service.pocket.logger
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.stories.api.PocketEndpoint
+import mozilla.components.service.pocket.stories.api.PocketResponse
/**
* Possible actions regarding the list of recommended stories.
+ *
+ * @param appContext Android Context. Prefer sending application context to limit the possibility of even small leaks.
+ * @param fetchClient the HTTP client to use for network requests.
*/
-class PocketStoriesUseCases {
+internal class PocketStoriesUseCases(
+ private val appContext: Context,
+ private val fetchClient: Client,
+) {
+ /**
+ * Download and persist an updated list of recommended stories.
+ */
+ internal val refreshStories by lazy { RefreshPocketStories(appContext, fetchClient) }
+
+ /**
+ * Get the list of available Pocket sponsored stories.
+ */
+ internal val getStories by lazy { GetPocketStories(appContext) }
+
+ /**
+ * Atomically update the number of impressions for a list of Pocket recommended stories.
+ */
+ internal val updateTimesShown by lazy { UpdateStoriesTimesShown(appContext) }
/**
* Allows for refreshing the list of pocket stories we have cached.
*
- * @param context Android Context. Prefer sending application context to limit the possibility of even small leaks.
+ * @param appContext Android Context. Prefer sending application context to limit the possibility
+ * of even small leaks.
+ * @param fetchClient the HTTP client to use for network requests.
*/
internal inner class RefreshPocketStories(
- @VisibleForTesting
- internal val context: Context
+ @get:VisibleForTesting
+ internal val appContext: Context = this@PocketStoriesUseCases.appContext,
+ @get:VisibleForTesting
+ internal val fetchClient: Client = this@PocketStoriesUseCases.fetchClient,
) {
/**
* Do a full download from Pocket -> persist locally cycle for recommended stories.
*/
suspend operator fun invoke(): Boolean {
- val client = fetchClient
- if (client == null) {
- logger.error("Cannot download new stories. Service has incomplete setup")
- return false
- }
-
- val pocket = getPocketEndpoint(client)
+ val pocket = getPocketEndpoint(fetchClient)
val response = pocket.getRecommendedStories()
if (response is PocketResponse.Success) {
- getPocketRepository(context)
+ getPocketRepository(appContext)
.addAllPocketApiStories(response.data)
return true
}
@@ -51,8 +68,13 @@ class PocketStoriesUseCases {
/**
* Allows for querying the list of locally available Pocket recommended stories.
+ *
+ * @param context [Context] used for various system interactions and libraries initializations.
*/
- internal inner class GetPocketStories(private val context: Context) {
+ internal inner class GetPocketStories(
+ @get:VisibleForTesting
+ internal val context: Context = this@PocketStoriesUseCases.appContext
+ ) {
/**
* Returns the current locally persisted list of Pocket recommended stories.
*/
@@ -64,8 +86,13 @@ class PocketStoriesUseCases {
/**
* Allows for atomically updating the [PocketRecommendedStory.timesShown] property of some recommended stories.
+ *
+ * @param context [Context] used for various system interactions and libraries initializations.
*/
- internal inner class UpdateStoriesTimesShown(private val context: Context) {
+ internal inner class UpdateStoriesTimesShown(
+ @get:VisibleForTesting
+ internal val context: Context = this@PocketStoriesUseCases.appContext
+ ) {
/**
* Update how many times certain stories were shown to the user.
*/
@@ -82,29 +109,4 @@ class PocketStoriesUseCases {
@VisibleForTesting
internal fun getPocketEndpoint(client: Client) = PocketEndpoint.newInstance(client)
-
- internal companion object {
- @VisibleForTesting internal var fetchClient: Client? = null
-
- /**
- * Convenience method for setting the the HTTP Client which will be used
- * for all REST communications with the Pocket server.
- *
- * Already downloaded data can still be queried but no new data can be downloaded until
- * this parameter is set.
- */
- internal fun initialize(client: Client) {
- this.fetchClient = client
- }
-
- /**
- * Convenience method for cleaning up any resources held for communicating with the Pocket server.
- *
- * Already downloaded data can still be queried but no new data can be downloaded until
- * [initialize] is used again.
- */
- internal fun reset() {
- fetchClient = null
- }
- }
}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/api/PocketApiStory.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketApiStory.kt
similarity index 95%
rename from components/service/pocket/src/main/java/mozilla/components/service/pocket/api/PocketApiStory.kt
rename to components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketApiStory.kt
index ec647e0807f..6cf05c208ac 100644
--- a/components/service/pocket/src/main/java/mozilla/components/service/pocket/api/PocketApiStory.kt
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketApiStory.kt
@@ -2,7 +2,7 @@
* 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.service.pocket.api
+package mozilla.components.service.pocket.stories.api
internal const val STRING_NOT_FOUND_DEFAULT_VALUE = ""
internal const val INT_NOT_FOUND_DEFAULT_VALUE = -1
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/api/PocketEndpoint.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketEndpoint.kt
similarity index 92%
rename from components/service/pocket/src/main/java/mozilla/components/service/pocket/api/PocketEndpoint.kt
rename to components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketEndpoint.kt
index 7223f331009..c026253d93b 100644
--- a/components/service/pocket/src/main/java/mozilla/components/service/pocket/api/PocketEndpoint.kt
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketEndpoint.kt
@@ -2,12 +2,12 @@
* 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.service.pocket.api
+package mozilla.components.service.pocket.stories.api
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import mozilla.components.concept.fetch.Client
-import mozilla.components.service.pocket.api.PocketEndpoint.Companion.newInstance
+import mozilla.components.service.pocket.stories.api.PocketEndpoint.Companion.newInstance
/**
* Makes requests to the Pocket API and returns the requested data.
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/api/PocketEndpointRaw.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketEndpointRaw.kt
similarity index 88%
rename from components/service/pocket/src/main/java/mozilla/components/service/pocket/api/PocketEndpointRaw.kt
rename to components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketEndpointRaw.kt
index 8290a450416..830d4fd0ad5 100644
--- a/components/service/pocket/src/main/java/mozilla/components/service/pocket/api/PocketEndpointRaw.kt
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketEndpointRaw.kt
@@ -2,14 +2,14 @@
* 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.service.pocket.api
+package mozilla.components.service.pocket.stories.api
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.Request
-import mozilla.components.service.pocket.api.PocketEndpointRaw.Companion.newInstance
-import mozilla.components.service.pocket.api.ext.fetchBodyOrNull
+import mozilla.components.service.pocket.ext.fetchBodyOrNull
+import mozilla.components.service.pocket.stories.api.PocketEndpointRaw.Companion.newInstance
/**
* Makes requests to the Pocket endpoint and returns the raw JSON data.
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/api/PocketJSONParser.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketJSONParser.kt
similarity index 98%
rename from components/service/pocket/src/main/java/mozilla/components/service/pocket/api/PocketJSONParser.kt
rename to components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketJSONParser.kt
index 4c085487cb7..762f399c163 100644
--- a/components/service/pocket/src/main/java/mozilla/components/service/pocket/api/PocketJSONParser.kt
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketJSONParser.kt
@@ -2,7 +2,7 @@
* 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.service.pocket.api
+package mozilla.components.service.pocket.stories.api
import androidx.annotation.VisibleForTesting
import mozilla.components.service.pocket.logger
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/api/PocketResponse.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketResponse.kt
similarity index 88%
rename from components/service/pocket/src/main/java/mozilla/components/service/pocket/api/PocketResponse.kt
rename to components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketResponse.kt
index 60a0b994aaa..23e8fdcf04b 100644
--- a/components/service/pocket/src/main/java/mozilla/components/service/pocket/api/PocketResponse.kt
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/api/PocketResponse.kt
@@ -2,7 +2,7 @@
* 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.service.pocket.api
+package mozilla.components.service.pocket.stories.api
/**
* A response from the Pocket API: the subclasses determine the type of the result and contain usable data.
@@ -28,12 +28,14 @@ internal sealed class PocketResponse {
* - null, then Failure
* - a Collection and empty, then Failure
* - a String and empty, then Failure
+ * - a Boolean and false, then Failure
* - otherwise, Success
*/
internal fun wrap(target: T?): PocketResponse = when (target) {
null -> Failure()
is Collection<*> -> if (target.isEmpty()) Failure() else Success(target)
is String -> if (target.isBlank()) Failure() else Success(target)
+ is Boolean -> if (target == false) Failure() else Success(target)
else -> Success(target)
}
}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabase.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabase.kt
index 4d5cf912bda..49a44170b55 100644
--- a/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabase.kt
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDatabase.kt
@@ -8,17 +8,32 @@ import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import mozilla.components.service.pocket.spocs.db.SpocEntity
+import mozilla.components.service.pocket.spocs.db.SpocImpressionEntity
+import mozilla.components.service.pocket.spocs.db.SpocsDao
/**
* Internal database for storing Pocket items.
*/
-@Database(entities = [PocketStoryEntity::class], version = 1)
+@Database(
+ entities = [
+ PocketStoryEntity::class,
+ SpocEntity::class,
+ SpocImpressionEntity::class
+ ],
+ version = 3
+)
internal abstract class PocketRecommendationsDatabase : RoomDatabase() {
abstract fun pocketRecommendationsDao(): PocketRecommendationsDao
+ abstract fun spocsDao(): SpocsDao
companion object {
private const val DATABASE_NAME = "pocket_recommendations"
const val TABLE_NAME_STORIES = "stories"
+ const val TABLE_NAME_SPOCS = "spocs"
+ const val TABLE_NAME_SPOCS_IMPRESSIONS = "spocs_impressions"
@Volatile
private var instance: PocketRecommendationsDatabase? = null
@@ -32,9 +47,90 @@ internal abstract class PocketRecommendationsDatabase : RoomDatabase() {
PocketRecommendationsDatabase::class.java,
DATABASE_NAME
)
+ .addMigrations(
+ Migrations.migration_1_2,
+ Migrations.migration_2_3,
+ Migrations.migration_1_3,
+ )
.build().also {
instance = it
}
}
}
}
+
+internal object Migrations {
+ val migration_1_2 = object : Migration(1, 2) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(
+ "CREATE TABLE IF NOT EXISTS " +
+ "`${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}` (" +
+ "`url` TEXT NOT NULL, " +
+ "`title` TEXT NOT NULL, " +
+ "`imageUrl` TEXT NOT NULL, " +
+ "`sponsor` TEXT NOT NULL, " +
+ "`clickShim` TEXT NOT NULL, " +
+ "`impressionShim` TEXT NOT NULL, " +
+ "PRIMARY KEY(`url`)" +
+ ")"
+ )
+ }
+ }
+
+ /**
+ * Migration for when adding support for pacing sponsored stories.
+ */
+ val migration_2_3 = object : Migration(2, 3) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ // There are many new columns added. Drop the old table allowing to start fresh.
+ // This migration is expected to only be needed in debug builds
+ // with the feature not being live in any Fenix release.
+ database.execSQL(
+ "DROP TABLE ${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}"
+ )
+
+ database.createNewSpocsTables()
+ }
+ }
+
+ /**
+ * Migration for when adding sponsored stories along with pacing support.
+ */
+ val migration_1_3 = object : Migration(1, 3) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.createNewSpocsTables()
+ }
+ }
+
+ private fun SupportSQLiteDatabase.createNewSpocsTables() {
+ execSQL(
+ "CREATE TABLE IF NOT EXISTS " +
+ "`${PocketRecommendationsDatabase.TABLE_NAME_SPOCS}` (" +
+ "`id` INTEGER NOT NULL, " +
+ "`url` TEXT NOT NULL, " +
+ "`title` TEXT NOT NULL, " +
+ "`imageUrl` TEXT NOT NULL, " +
+ "`sponsor` TEXT NOT NULL, " +
+ "`clickShim` TEXT NOT NULL, " +
+ "`impressionShim` TEXT NOT NULL, " +
+ "`priority` INTEGER NOT NULL, " +
+ "`lifetimeCapCount` INTEGER NOT NULL, " +
+ "`flightCapCount` INTEGER NOT NULL, " +
+ "`flightCapPeriod` INTEGER NOT NULL, " +
+ "PRIMARY KEY(`id`)" +
+ ")"
+ )
+
+ execSQL(
+ "CREATE TABLE IF NOT EXISTS " +
+ "`${PocketRecommendationsDatabase.TABLE_NAME_SPOCS_IMPRESSIONS}` (" +
+ "`spocId` INTEGER NOT NULL, " +
+ "`impressionId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
+ "`impressionDateInSeconds` INTEGER NOT NULL, " +
+ "FOREIGN KEY(`spocId`) " +
+ "REFERENCES `spocs`(`id`) " +
+ "ON UPDATE NO ACTION ON DELETE CASCADE " +
+ ")"
+ )
+ }
+}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/ext/Mappers.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/ext/Mappers.kt
deleted file mode 100644
index b0cee77f83a..00000000000
--- a/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/ext/Mappers.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/* 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.service.pocket.stories.ext
-
-import androidx.annotation.VisibleForTesting
-import mozilla.components.service.pocket.PocketRecommendedStory
-import mozilla.components.service.pocket.api.PocketApiStory
-import mozilla.components.service.pocket.stories.db.PocketLocalStoryTimesShown
-import mozilla.components.service.pocket.stories.db.PocketStoryEntity
-
-@VisibleForTesting
-internal const val DEFAULT_CATEGORY = "general"
-@VisibleForTesting
-internal const val DEFAULT_TIMES_SHOWN = 0L
-
-/**
- * Map Pocket API objects to the object type that we persist locally.
- */
-internal fun PocketApiStory.toPocketLocalStory(): PocketStoryEntity =
- PocketStoryEntity(
- url, title, imageUrl, publisher, category, timeToRead, DEFAULT_TIMES_SHOWN
- )
-
-/**
- * Map Room entities to the object type that we expose to service clients.
- */
-internal fun PocketStoryEntity.toPocketRecommendedStory(): PocketRecommendedStory =
- PocketRecommendedStory(
- url = url,
- title = title,
- imageUrl = imageUrl,
- publisher = publisher,
- category = if (category.isNotBlank()) category else DEFAULT_CATEGORY,
- timeToRead = timeToRead,
- timesShown = timesShown
- )
-
-/**
- * Maps an object of the type exposed to clients to one that can partially update only the "timesShown"
- * property of the type we persist locally.
- */
-internal fun PocketRecommendedStory.toPartialTimeShownUpdate(): PocketLocalStoryTimesShown =
- PocketLocalStoryTimesShown(url, timesShown)
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/DeleteSpocsProfileWorker.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/DeleteSpocsProfileWorker.kt
new file mode 100644
index 00000000000..afe1d40bf5e
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/DeleteSpocsProfileWorker.kt
@@ -0,0 +1,36 @@
+/* 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.service.pocket.update
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.service.pocket.GlobalDependencyProvider
+
+/**
+ * WorkManager Worker used for deleting the profile used for downloading Pocket sponsored stories.
+ */
+internal class DeleteSpocsProfileWorker(
+ context: Context,
+ params: WorkerParameters
+) : CoroutineWorker(context, params) {
+
+ override suspend fun doWork(): Result {
+ return withContext(Dispatchers.IO) {
+ if (GlobalDependencyProvider.SponsoredStories.useCases?.deleteProfile?.invoke() == true) {
+ Result.success()
+ } else {
+ Result.retry()
+ }
+ }
+ }
+
+ internal companion object {
+ const val DELETE_SPOCS_PROFILE_WORK_TAG =
+ "mozilla.components.feature.pocket.spocs.profile.delete.work.tag"
+ }
+}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/update/PocketStoriesRefreshScheduler.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/PocketStoriesRefreshScheduler.kt
similarity index 92%
rename from components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/update/PocketStoriesRefreshScheduler.kt
rename to components/service/pocket/src/main/java/mozilla/components/service/pocket/update/PocketStoriesRefreshScheduler.kt
index b9eb2c5aa54..60b1d300191 100644
--- a/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/update/PocketStoriesRefreshScheduler.kt
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/PocketStoriesRefreshScheduler.kt
@@ -2,7 +2,7 @@
* 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.service.pocket.stories.update
+package mozilla.components.service.pocket.update
import android.content.Context
import androidx.annotation.VisibleForTesting
@@ -14,7 +14,7 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import mozilla.components.service.pocket.PocketStoriesConfig
import mozilla.components.service.pocket.logger
-import mozilla.components.service.pocket.stories.update.RefreshPocketWorker.Companion.REFRESH_WORK_TAG
+import mozilla.components.service.pocket.update.RefreshPocketWorker.Companion.REFRESH_WORK_TAG
import mozilla.components.support.base.worker.Frequency
/**
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/update/RefreshPocketWorker.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/RefreshPocketWorker.kt
similarity index 79%
rename from components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/update/RefreshPocketWorker.kt
rename to components/service/pocket/src/main/java/mozilla/components/service/pocket/update/RefreshPocketWorker.kt
index 03d25a0a329..9e33af199d1 100644
--- a/components/service/pocket/src/main/java/mozilla/components/service/pocket/stories/update/RefreshPocketWorker.kt
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/RefreshPocketWorker.kt
@@ -2,27 +2,26 @@
* 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.service.pocket.stories.update
+package mozilla.components.service.pocket.update
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
-import mozilla.components.service.pocket.stories.PocketStoriesUseCases
+import mozilla.components.service.pocket.GlobalDependencyProvider
/**
* WorkManager Worker used for downloading and persisting locally a new list of Pocket recommended stories.
*/
internal class RefreshPocketWorker(
- private val context: Context,
+ context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return withContext(Dispatchers.IO) {
- if (PocketStoriesUseCases().RefreshPocketStories(context).invoke()
- ) {
+ if (GlobalDependencyProvider.RecommendedStories.useCases?.refreshStories?.invoke() == true) {
Result.success()
} else {
Result.retry()
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/RefreshSpocsWorker.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/RefreshSpocsWorker.kt
new file mode 100644
index 00000000000..012a5e635b3
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/RefreshSpocsWorker.kt
@@ -0,0 +1,36 @@
+/* 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.service.pocket.update
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.service.pocket.GlobalDependencyProvider
+
+/**
+ * WorkManager Worker used for downloading and persisting locally a new list of Pocket recommended stories.
+ */
+internal class RefreshSpocsWorker(
+ context: Context,
+ params: WorkerParameters
+) : CoroutineWorker(context, params) {
+
+ override suspend fun doWork(): Result {
+ return withContext(Dispatchers.IO) {
+ if (GlobalDependencyProvider.SponsoredStories.useCases?.refreshStories?.invoke() == true) {
+ Result.success()
+ } else {
+ Result.retry()
+ }
+ }
+ }
+
+ internal companion object {
+ const val REFRESH_SPOCS_WORK_TAG =
+ "mozilla.components.feature.pocket.spocs.refresh.work.tag"
+ }
+}
diff --git a/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/SpocsRefreshScheduler.kt b/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/SpocsRefreshScheduler.kt
new file mode 100644
index 00000000000..311a26474d5
--- /dev/null
+++ b/components/service/pocket/src/main/java/mozilla/components/service/pocket/update/SpocsRefreshScheduler.kt
@@ -0,0 +1,94 @@
+/* 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.service.pocket.update
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequest
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.PeriodicWorkRequest
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import mozilla.components.service.pocket.PocketStoriesConfig
+import mozilla.components.service.pocket.logger
+import mozilla.components.service.pocket.update.DeleteSpocsProfileWorker.Companion.DELETE_SPOCS_PROFILE_WORK_TAG
+import mozilla.components.service.pocket.update.RefreshSpocsWorker.Companion.REFRESH_SPOCS_WORK_TAG
+import mozilla.components.support.base.worker.Frequency
+
+/**
+ * Class used to schedule Pocket recommended stories refresh.
+ */
+internal class SpocsRefreshScheduler(
+ private val pocketStoriesConfig: PocketStoriesConfig
+) {
+ internal fun schedulePeriodicRefreshes(context: Context) {
+ logger.info("Scheduling sponsored stories background refresh")
+
+ val refreshWork = createPeriodicRefreshWorkerRequest(
+ frequency = pocketStoriesConfig.sponsoredStoriesRefreshFrequency
+ )
+
+ getWorkManager(context)
+ .enqueueUniquePeriodicWork(REFRESH_SPOCS_WORK_TAG, ExistingPeriodicWorkPolicy.KEEP, refreshWork)
+ }
+
+ internal fun stopPeriodicRefreshes(context: Context) {
+ getWorkManager(context)
+ .cancelAllWorkByTag(REFRESH_SPOCS_WORK_TAG)
+ }
+
+ internal fun scheduleProfileDeletion(context: Context) {
+ logger.info("Scheduling sponsored stories profile deletion")
+
+ val deleteProfileWork = createOneTimeProfileDeletionWorkerRequest()
+
+ getWorkManager(context)
+ .enqueueUniqueWork(DELETE_SPOCS_PROFILE_WORK_TAG, ExistingWorkPolicy.KEEP, deleteProfileWork)
+ }
+
+ internal fun stopProfileDeletion(context: Context) {
+ getWorkManager(context)
+ .cancelAllWorkByTag(DELETE_SPOCS_PROFILE_WORK_TAG)
+ }
+
+ @VisibleForTesting
+ internal fun createOneTimeProfileDeletionWorkerRequest(): OneTimeWorkRequest {
+ val constraints = getWorkerConstrains()
+
+ return OneTimeWorkRequestBuilder()
+ .apply {
+ setConstraints(constraints)
+ addTag(DELETE_SPOCS_PROFILE_WORK_TAG)
+ }
+ .build()
+ }
+
+ @VisibleForTesting
+ internal fun createPeriodicRefreshWorkerRequest(
+ frequency: Frequency
+ ): PeriodicWorkRequest {
+ val constraints = getWorkerConstrains()
+
+ return PeriodicWorkRequestBuilder(
+ frequency.repeatInterval,
+ frequency.repeatIntervalTimeUnit
+ ).apply {
+ setConstraints(constraints)
+ addTag(REFRESH_SPOCS_WORK_TAG)
+ }.build()
+ }
+
+ @VisibleForTesting
+ internal fun getWorkerConstrains() = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+
+ @VisibleForTesting
+ internal fun getWorkManager(context: Context) = WorkManager.getInstance(context)
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/GlobalDependencyProviderTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/GlobalDependencyProviderTest.kt
new file mode 100644
index 00000000000..a87f46887d1
--- /dev/null
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/GlobalDependencyProviderTest.kt
@@ -0,0 +1,50 @@
+/* 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.service.pocket
+
+import mozilla.components.service.pocket.spocs.SpocsUseCases
+import mozilla.components.service.pocket.stories.PocketStoriesUseCases
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Test
+
+class GlobalDependencyProviderTest {
+ @Test
+ fun `GIVEN RecommendedStories WHEN initializing THEN store the provided arguments`() {
+ val useCases: PocketStoriesUseCases = mock()
+
+ GlobalDependencyProvider.RecommendedStories.initialize(useCases)
+
+ assertSame(useCases, GlobalDependencyProvider.RecommendedStories.useCases)
+ }
+
+ @Test
+ fun `GIVEN RecommendedStories WHEN resetting THEN clear all current state`() {
+ GlobalDependencyProvider.RecommendedStories.initialize(mock())
+
+ GlobalDependencyProvider.RecommendedStories.reset()
+
+ assertNull(GlobalDependencyProvider.RecommendedStories.useCases)
+ }
+
+ @Test
+ fun `GIVEN SponsoredStories WHEN initializing THEN store the provided arguments`() {
+ val useCases: SpocsUseCases = mock()
+
+ GlobalDependencyProvider.SponsoredStories.initialize(useCases)
+
+ assertSame(useCases, GlobalDependencyProvider.SponsoredStories.useCases)
+ }
+
+ @Test
+ fun `GIVEN SponsoredStories WHEN resetting THEN clear all current state`() {
+ GlobalDependencyProvider.SponsoredStories.initialize(mock())
+
+ GlobalDependencyProvider.SponsoredStories.reset()
+
+ assertNull(GlobalDependencyProvider.SponsoredStories.useCases)
+ }
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketRecommendedStoryTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketRecommendedStoryTest.kt
deleted file mode 100644
index 559ae61085d..00000000000
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketRecommendedStoryTest.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/* 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.service.pocket
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import mozilla.components.service.pocket.helpers.assertClassVisibility
-import org.junit.Test
-import org.junit.runner.RunWith
-import kotlin.reflect.KVisibility
-
-@RunWith(AndroidJUnit4::class)
-class PocketRecommendedStoryTest {
-
- // This is the domain data type we expose to clients. Needs to be public.
- @Test
- fun `GIVEN a PocketRecommendedStory THEN its visibility is public`() {
- assertClassVisibility(PocketRecommendedStory::class, KVisibility.PUBLIC)
- }
-}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesConfigTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesConfigTest.kt
index a130193562f..17a8959cf85 100644
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesConfigTest.kt
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesConfigTest.kt
@@ -9,6 +9,7 @@ import mozilla.components.service.pocket.helpers.assertClassVisibility
import mozilla.components.support.base.worker.Frequency
import mozilla.components.support.test.mock
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.reflect.KVisibility
@@ -29,6 +30,25 @@ class PocketStoriesConfigTest {
assertEquals(defaultFrequency.repeatIntervalTimeUnit, config.frequency.repeatIntervalTimeUnit)
}
+ @Test
+ fun `WHEN instantiating a PocketStoriesConfig THEN sponsored stories refresh frequency has a default value`() {
+ val config = PocketStoriesConfig(mock())
+
+ val defaultFrequency = Frequency(
+ DEFAULT_SPONSORED_STORIES_REFRESH_INTERVAL,
+ DEFAULT_SPONSORED_STORIES_REFRESH_TIMEUNIT
+ )
+ assertEquals(defaultFrequency.repeatInterval, config.sponsoredStoriesRefreshFrequency.repeatInterval)
+ assertEquals(defaultFrequency.repeatIntervalTimeUnit, config.sponsoredStoriesRefreshFrequency.repeatIntervalTimeUnit)
+ }
+
+ @Test
+ fun `WHEN instantiating a PocketStoriesConfig THEN profile is by default null`() {
+ val config = PocketStoriesConfig(mock())
+
+ assertNull(config.profile)
+ }
+
@Test
fun `GIVEN a Frequency THEN its visibility is internal`() {
assertClassVisibility(Frequency::class, KVisibility.PUBLIC)
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesServiceTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesServiceTest.kt
index 0ae775d39de..bfbe4b3c946 100644
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesServiceTest.kt
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoriesServiceTest.kt
@@ -5,23 +5,50 @@
package mozilla.components.service.pocket
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import mozilla.components.service.pocket.helpers.assertConstructorsVisibility
+import mozilla.components.service.pocket.spocs.SpocsUseCases
+import mozilla.components.service.pocket.spocs.SpocsUseCases.GetSponsoredStories
+import mozilla.components.service.pocket.spocs.SpocsUseCases.RecordImpression
+import mozilla.components.service.pocket.stories.PocketStoriesUseCases
+import mozilla.components.service.pocket.stories.PocketStoriesUseCases.GetPocketStories
+import mozilla.components.service.pocket.stories.PocketStoriesUseCases.UpdateStoriesTimesShown
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
-import mozilla.components.support.test.whenever
+import org.junit.After
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+import java.util.UUID
import kotlin.reflect.KVisibility
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class PocketStoriesServiceTest {
+ private val storiesUseCases: PocketStoriesUseCases = mock()
+ private val spocsUseCases: SpocsUseCases = mock()
private val service = PocketStoriesService(testContext, PocketStoriesConfig(mock())).also {
- it.scheduler = mock()
- it.getStoriesUsecase = mock()
+ it.storiesRefreshScheduler = mock()
+ it.spocsRefreshscheduler = mock()
+ it.storiesUseCases = storiesUseCases
+ it.spocsUseCases = spocsUseCases
+ }
+
+ @After
+ fun teardown() {
+ GlobalDependencyProvider.SponsoredStories.reset()
+ GlobalDependencyProvider.RecommendedStories.reset()
}
@Test
@@ -30,26 +57,163 @@ class PocketStoriesServiceTest {
}
@Test
- fun `GIVEN PocketStoriesService WHEN startPeriodicStoriesRefresh THEN scheduler#schedulePeriodicRefreshes should be called`() {
+ fun `GIVEN PocketStoriesService WHEN startPeriodicStoriesRefresh THEN persist dependencies and schedule stories refresh`() {
service.startPeriodicStoriesRefresh()
- verify(service.scheduler).schedulePeriodicRefreshes(any())
+ assertNotNull(GlobalDependencyProvider.RecommendedStories.useCases)
+ verify(service.storiesRefreshScheduler).schedulePeriodicRefreshes(any())
}
@Test
- fun `GIVEN PocketStoriesService WHEN stopPeriodicStoriesRefresh THEN scheduler#stopPeriodicRefreshes should be called`() {
+ fun `GIVEN PocketStoriesService WHEN stopPeriodicStoriesRefresh THEN stop refreshing stories and clear dependencies`() {
service.stopPeriodicStoriesRefresh()
- verify(service.scheduler).stopPeriodicRefreshes(any())
+ verify(service.storiesRefreshScheduler).stopPeriodicRefreshes(any())
+ assertNull(GlobalDependencyProvider.RecommendedStories.useCases)
}
@Test
- fun `GIVEN PocketStoriesService WHEN getStories THEN getStoriesUsecase should return`() = runBlocking {
+ fun `GIVEN PocketStoriesService is initialized with a valid profile WHEN called to start periodic refreshes THEN persist dependencies, cancel profile deletion and schedule stories refresh`() {
+ val client: Client = mock()
+ val profileId = UUID.randomUUID()
+ val appId = "test"
+ val service = PocketStoriesService(
+ context = testContext,
+ pocketStoriesConfig = PocketStoriesConfig(
+ client = client,
+ profile = Profile(
+ profileId = profileId,
+ appId = appId
+ )
+ )
+ ).apply {
+ spocsRefreshscheduler = mock()
+ }
+
+ service.startPeriodicSponsoredStoriesRefresh()
+
+ assertNotNull(GlobalDependencyProvider.SponsoredStories.useCases)
+ verify(service.spocsRefreshscheduler).stopProfileDeletion(any())
+ verify(service.spocsRefreshscheduler).schedulePeriodicRefreshes(any())
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService is initialized with an invalid profile WHEN called to start periodic refreshes THEN don't schedule periodic refreshes and don't persist dependencies`() {
+ val service = PocketStoriesService(
+ context = testContext,
+ pocketStoriesConfig = PocketStoriesConfig(
+ client = mock(),
+ profile = null
+ )
+ ).apply {
+ spocsRefreshscheduler = mock()
+ }
+
+ service.startPeriodicSponsoredStoriesRefresh()
+
+ verify(service.spocsRefreshscheduler, never()).schedulePeriodicRefreshes(any())
+ assertNull(GlobalDependencyProvider.SponsoredStories.useCases)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService WHEN called to stop periodic refreshes THEN stop refreshing stories`() {
+ // Mock periodic refreshes were started previously and profile details were set.
+ // Now they will have to be cleaned.
+ GlobalDependencyProvider.SponsoredStories.initialize(mock())
+ service.spocsRefreshscheduler = mock()
+
+ service.stopPeriodicSponsoredStoriesRefresh()
+
+ verify(service.spocsRefreshscheduler).stopPeriodicRefreshes(any())
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService WHEN getStories THEN stories useCases should return`() = runTest {
val stories = listOf(mock())
- whenever(service.getStoriesUsecase.invoke()).thenReturn(stories)
+ val getStoriesUseCase: GetPocketStories = mock()
+ doReturn(stories).`when`(getStoriesUseCase).invoke()
+ doReturn(getStoriesUseCase).`when`(storiesUseCases).getStories
val result = service.getStories()
assertEquals(stories, result)
}
+
+ @Test
+ fun `GIVEN PocketStoriesService WHEN updateStoriesTimesShown THEN delegate to spocs useCases`() = runTest {
+ val updateTimesShownUseCase: UpdateStoriesTimesShown = mock()
+ doReturn(updateTimesShownUseCase).`when`(storiesUseCases).updateTimesShown
+ val stories = listOf(mock())
+
+ service.updateStoriesTimesShown(stories)
+
+ verify(updateTimesShownUseCase).invoke(stories)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService WHEN getSponsoredStories THEN delegate to spocs useCases`() = runTest {
+ val noProfileResponse = service.getSponsoredStories()
+ assertTrue(noProfileResponse.isEmpty())
+
+ val stories = listOf(mock())
+ val getStoriesUseCase: GetSponsoredStories = mock()
+ doReturn(stories).`when`(getStoriesUseCase).invoke()
+ doReturn(getStoriesUseCase).`when`(spocsUseCases).getStories
+ val existingProfileResponse = service.getSponsoredStories()
+ assertEquals(stories, existingProfileResponse)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService is initialized with a valid profile WHEN called to delete profile THEN persist dependencies, cancel stories refresh and schedule profile deletion`() {
+ val client: Client = mock()
+ val profileId = UUID.randomUUID()
+ val appId = "test"
+ val service = PocketStoriesService(
+ context = testContext,
+ pocketStoriesConfig = PocketStoriesConfig(
+ client = client,
+ profile = Profile(
+ profileId = profileId,
+ appId = appId
+ )
+ )
+ ).apply {
+ spocsRefreshscheduler = mock()
+ }
+
+ service.deleteProfile()
+
+ assertNotNull(GlobalDependencyProvider.SponsoredStories.useCases)
+ verify(service.spocsRefreshscheduler).stopPeriodicRefreshes(any())
+ verify(service.spocsRefreshscheduler).scheduleProfileDeletion(any())
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService is initialized with an invalid profile WHEN called to delete profile THEN don't schedule profile deletion and don't persist dependencies`() {
+ val service = PocketStoriesService(
+ context = testContext,
+ pocketStoriesConfig = PocketStoriesConfig(
+ client = mock(),
+ profile = null
+ )
+ ).apply {
+ spocsRefreshscheduler = mock()
+ }
+
+ service.deleteProfile()
+
+ verify(service.spocsRefreshscheduler, never()).scheduleProfileDeletion(any())
+ assertNull(GlobalDependencyProvider.SponsoredStories.useCases)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesService WHEN recordStoriesImpressions THEN delegate to spocs useCases`() = runTest {
+ val recordImpressionsUseCase: RecordImpression = mock()
+ doReturn(recordImpressionsUseCase).`when`(spocsUseCases).recordImpression
+ val storiesIds = listOf(22, 33)
+
+ service.recordStoriesImpressions(storiesIds)
+
+ verify(recordImpressionsUseCase).invoke(storiesIds)
+ }
}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoryTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoryTest.kt
new file mode 100644
index 00000000000..3e4eaae44e7
--- /dev/null
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/PocketStoryTest.kt
@@ -0,0 +1,88 @@
+/* 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.service.pocket
+
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
+import mozilla.components.service.pocket.helpers.assertConstructorsVisibility
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import kotlin.reflect.KVisibility
+
+class PocketStoryTest {
+ @Test
+ fun `GIVEN PocketSponsoredStory THEN it should be publicly available`() {
+ assertConstructorsVisibility(PocketSponsoredStory::class, KVisibility.PUBLIC)
+ }
+
+ @Test
+ fun `GIVEN PocketSponsoredStoryCaps THEN it should be publicly available`() {
+ assertConstructorsVisibility(PocketRecommendedStory::class, KVisibility.PUBLIC)
+ }
+
+ @Test
+ fun `GIVEN PocketRecommendedStory THEN it should be publicly available`() {
+ assertConstructorsVisibility(PocketRecommendedStory::class, KVisibility.PUBLIC)
+ }
+
+ @Test
+ fun `GIVEN a PocketRecommendedStory WHEN it's title is accessed from parent THEN it returns the previously set value`() {
+ val pocketRecommendedStory = PocketRecommendedStory(
+ title = "testTitle", url = "", imageUrl = "", publisher = "", category = "", timeToRead = 0, timesShown = 0
+ )
+
+ val result = (pocketRecommendedStory as PocketStory).title
+
+ assertEquals("testTitle", result)
+ }
+
+ @Test
+ fun `GIVEN a PocketRecommendedStory WHEN it's url is accessed from parent THEN it returns the previously set value`() {
+ val pocketRecommendedStory = PocketRecommendedStory(
+ title = "", url = "testUrl", imageUrl = "", publisher = "", category = "", timeToRead = 0, timesShown = 0
+ )
+
+ val result = (pocketRecommendedStory as PocketStory).url
+
+ assertEquals("testUrl", result)
+ }
+
+ @Test
+ fun `GIVEN a PocketSponsoredStory WHEN it's title is accessed from parent THEN it returns the previously set value`() {
+ val pocketRecommendedStory = PocketSponsoredStory(
+ id = 1,
+ title = "testTitle",
+ url = "",
+ imageUrl = "",
+ sponsor = "",
+ shim = mock(),
+ priority = 11,
+ caps = mock(),
+ )
+
+ val result = (pocketRecommendedStory as PocketStory).title
+
+ assertEquals("testTitle", result)
+ }
+
+ @Test
+ fun `GIVEN a PocketSponsoredStory WHEN it's url is accessed from parent THEN it returns the previously set value`() {
+ val pocketRecommendedStory = PocketSponsoredStory(
+ id = 2,
+ title = "",
+ url = "testUrl",
+ imageUrl = "",
+ sponsor = "",
+ shim = mock(),
+ priority = 33,
+ caps = mock(),
+ )
+
+ val result = (pocketRecommendedStory as PocketStory).url
+
+ assertEquals("testUrl", result)
+ }
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/api/ext/ConceptFetchKtTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/ConceptFetchKtTest.kt
similarity index 98%
rename from components/service/pocket/src/test/java/mozilla/components/service/pocket/api/ext/ConceptFetchKtTest.kt
rename to components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/ConceptFetchKtTest.kt
index 66762ebc9e9..0e436fc2eef 100644
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/api/ext/ConceptFetchKtTest.kt
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/ConceptFetchKtTest.kt
@@ -2,7 +2,7 @@
* 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.service.pocket.api.ext
+package mozilla.components.service.pocket.ext
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.MutableHeaders
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/ext/MappersKtTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/MappersKtTest.kt
similarity index 62%
rename from components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/ext/MappersKtTest.kt
rename to components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/MappersKtTest.kt
index 9fb8d8fe91e..0cf5c6a6bb4 100644
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/ext/MappersKtTest.kt
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/MappersKtTest.kt
@@ -2,12 +2,13 @@
* 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.service.pocket.stories.ext
+package mozilla.components.service.pocket.ext
import mozilla.components.service.pocket.helpers.PocketTestResources
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
import org.junit.Test
import kotlin.reflect.full.memberProperties
@@ -71,4 +72,43 @@ class MappersKtTest {
assertSame(story.url, result.url)
assertSame(story.timesShown, result.timesShown)
}
+
+ @Test
+ fun `GIVEN a spoc downloaded from Internet WHEN it is converted to a local spoc THEN a one to one mapping is made`() {
+ val apiStory = PocketTestResources.apiExpectedPocketSpocs[0]
+
+ val result = apiStory.toLocalSpoc()
+
+ assertEquals(apiStory.flightId, result.id)
+ assertSame(apiStory.title, result.title)
+ assertSame(apiStory.url, result.url)
+ assertSame(apiStory.imageSrc, result.imageUrl)
+ assertSame(apiStory.sponsor, result.sponsor)
+ assertSame(apiStory.shim.click, result.clickShim)
+ assertSame(apiStory.shim.impression, result.impressionShim)
+ assertEquals(apiStory.priority, result.priority)
+ assertEquals(apiStory.caps.lifetimeCount, result.lifetimeCapCount)
+ assertEquals(apiStory.caps.flightCount, result.flightCapCount)
+ assertEquals(apiStory.caps.flightPeriod, result.flightCapPeriod)
+ }
+
+ @Test
+ fun `GIVEN a local spoc WHEN it is converted to be exposed to clients THEN a one to one mapping is made`() {
+ val localStory = PocketTestResources.dbExpectedPocketSpoc
+
+ val result = localStory.toPocketSponsoredStory()
+
+ assertEquals(localStory.id, result.id)
+ assertSame(localStory.title, result.title)
+ assertSame(localStory.url, result.url)
+ assertSame(localStory.imageUrl, result.imageUrl)
+ assertSame(localStory.sponsor, result.sponsor)
+ assertSame(localStory.clickShim, result.shim.click)
+ assertSame(localStory.impressionShim, result.shim.impression)
+ assertEquals(localStory.priority, result.priority)
+ assertEquals(localStory.lifetimeCapCount, result.caps.lifetimeCount)
+ assertEquals(localStory.flightCapCount, result.caps.flightCount)
+ assertEquals(localStory.flightCapPeriod, result.caps.flightPeriod)
+ assertTrue(result.caps.currentImpressions.isEmpty())
+ }
}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/PocketStoryKtTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/PocketStoryKtTest.kt
new file mode 100644
index 00000000000..f8bef315444
--- /dev/null
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/ext/PocketStoryKtTest.kt
@@ -0,0 +1,138 @@
+/* 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.service.pocket.ext
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
+import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryCaps
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+
+@RunWith(AndroidJUnit4::class)
+class PocketStoryKtTest {
+ private val nowInSeconds = System.currentTimeMillis() / 1000
+ private val flightPeriod = 100
+ private val flightImpression1 = nowInSeconds - flightPeriod / 2
+ private val flightImpression2 = nowInSeconds - flightPeriod / 3
+ private val currentImpressions = listOf(
+ nowInSeconds - flightPeriod * 2, // older impression that doesn't fit the flight period
+ flightImpression1,
+ flightImpression2
+ )
+
+ @Test
+ fun `GIVEN sponsored story impressions recorded WHEN asking for the current flight impression THEN return all impressions in flight period`() {
+ val storyCaps = PocketSponsoredStoryCaps(
+ currentImpressions = currentImpressions,
+ lifetimeCount = 10,
+ flightCount = 5,
+ flightPeriod = flightPeriod
+ )
+ val story: PocketSponsoredStory = mock()
+ doReturn(storyCaps).`when`(story).caps
+
+ val result = story.getCurrentFlightImpressions()
+
+ assertEquals(listOf(flightImpression1, flightImpression2), result)
+ }
+
+ @Test
+ fun `GIVEN sponsored story impressions recorded WHEN asking if lifetime impressions reached THEN return false if not`() {
+ val storyCaps = PocketSponsoredStoryCaps(
+ currentImpressions = currentImpressions,
+ lifetimeCount = 10,
+ flightCount = 5,
+ flightPeriod = flightPeriod
+ )
+ val story: PocketSponsoredStory = mock()
+ doReturn(storyCaps).`when`(story).caps
+
+ val result = story.hasLifetimeImpressionsLimitReached()
+
+ assertFalse(result)
+ }
+
+ @Test
+ fun `GIVEN sponsored story impressions recorded WHEN asking if lifetime impressions reached THEN return true if so`() {
+ val storyCaps = PocketSponsoredStoryCaps(
+ currentImpressions = currentImpressions,
+ lifetimeCount = 3,
+ flightCount = 3,
+ flightPeriod = flightPeriod
+ )
+ val story: PocketSponsoredStory = mock()
+ doReturn(storyCaps).`when`(story).caps
+
+ val result = story.hasLifetimeImpressionsLimitReached()
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `GIVEN sponsored story impressions recorded WHEN asking if flight impressions reached THEN return false if not`() {
+ val storyCaps = PocketSponsoredStoryCaps(
+ currentImpressions = currentImpressions,
+ lifetimeCount = 10,
+ flightCount = 5,
+ flightPeriod = flightPeriod
+ )
+ val story: PocketSponsoredStory = mock()
+ doReturn(storyCaps).`when`(story).caps
+
+ val result = story.hasFlightImpressionsLimitReached()
+
+ assertFalse(result)
+ }
+
+ @Test
+ fun `GIVEN sponsored story impressions recorded WHEN asking if flight impressions reached THEN return true if so`() {
+ val storyCaps = PocketSponsoredStoryCaps(
+ currentImpressions = currentImpressions,
+ lifetimeCount = 3,
+ flightCount = 2,
+ flightPeriod = flightPeriod
+ )
+ val story: PocketSponsoredStory = mock()
+ doReturn(storyCaps).`when`(story).caps
+
+ val result = story.hasFlightImpressionsLimitReached()
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `GIVEN a sponsored story WHEN recording a new impression THEN update the same story to contain a new impression recorded in seconds`() {
+ val story = PocketTestResources.dbExpectedPocketSpoc.toPocketSponsoredStory(currentImpressions)
+
+ assertEquals(3, story.caps.currentImpressions.size)
+ val result = story.recordNewImpression()
+
+ assertEquals(story.id, result.id)
+ assertSame(story.title, result.title)
+ assertSame(story.url, result.url)
+ assertSame(story.imageUrl, result.imageUrl)
+ assertSame(story.sponsor, result.sponsor)
+ assertSame(story.shim, result.shim)
+ assertEquals(story.priority, result.priority)
+ assertEquals(story.caps.lifetimeCount, result.caps.lifetimeCount)
+ assertEquals(story.caps.flightCount, result.caps.flightCount)
+ assertEquals(story.caps.flightPeriod, result.caps.flightPeriod)
+
+ assertEquals(4, result.caps.currentImpressions.size)
+ assertEquals(currentImpressions, result.caps.currentImpressions.take(3))
+ // Check if a new impression has been added for around this current time.
+ assertTrue(
+ LongRange(nowInSeconds - 5, nowInSeconds + 5)
+ .contains(result.caps.currentImpressions[3])
+ )
+ }
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/Assert.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/Assert.kt
index 90c48b3401d..1219dc139e5 100644
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/Assert.kt
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/Assert.kt
@@ -8,7 +8,7 @@ import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.MutableHeaders
import mozilla.components.concept.fetch.Request
import mozilla.components.concept.fetch.Response
-import mozilla.components.service.pocket.api.PocketResponse
+import mozilla.components.service.pocket.stories.api.PocketResponse
import mozilla.components.support.test.any
import mozilla.components.support.test.whenever
import org.junit.Assert.assertEquals
@@ -77,3 +77,7 @@ fun assertResponseIsClosed(client: Client, response: Response, makeRequest: () -
fun assertResponseIsFailure(response: Any) {
assertEquals(PocketResponse.Failure::class.java, response.javaClass)
}
+
+fun assertResponseIsSuccess(response: Any) {
+ assertEquals(PocketResponse.Success::class.java, response.javaClass)
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/PocketTestResources.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/PocketTestResources.kt
index 0e805c69eac..cb822a8d048 100644
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/PocketTestResources.kt
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/helpers/PocketTestResources.kt
@@ -4,8 +4,12 @@
package mozilla.components.service.pocket.helpers
-import mozilla.components.service.pocket.PocketRecommendedStory
-import mozilla.components.service.pocket.api.PocketApiStory
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.spocs.api.ApiSpoc
+import mozilla.components.service.pocket.spocs.api.ApiSpocCaps
+import mozilla.components.service.pocket.spocs.api.ApiSpocShim
+import mozilla.components.service.pocket.spocs.db.SpocEntity
+import mozilla.components.service.pocket.stories.api.PocketApiStory
import mozilla.components.service.pocket.stories.db.PocketStoryEntity
private const val POCKET_DIR = "pocket"
@@ -18,6 +22,10 @@ internal object PocketTestResources {
"$POCKET_DIR/stories_recommendations_response.json"
)!!.readText()
+ val pocketEndpointThreeSpocsResponse = this::class.java.classLoader!!.getResource(
+ "$POCKET_DIR/sponsored_stories_response.json"
+ )!!.readText()
+
val apiExpectedPocketStoriesRecommendations: List = listOf(
PocketApiStory(
title = "How to Remember Anything You Really Want to Remember, Backed by Science",
@@ -61,6 +69,60 @@ internal object PocketTestResources {
)
)
+ val apiExpectedPocketSpocs: List = listOf(
+ ApiSpoc(
+ flightId = 191739319,
+ title = "Eating Keto Has Never Been So Easy With Green Chef",
+ url = "https://i.geistm.com/l/GC_7ReasonsKetoV2_Journiest?bcid=601c567ac5b18a0414cce1d4&bhid=624f3ea9adad7604086ac6b3&utm_content=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off_601c567ac5b18a0414cce1d4_624f3ea9adad7604086ac6b3&tv=su4&ct=NAT-PK-PROS-130OFF5WEEK-037&utm_medium=DB&utm_source=pocket~geistm&utm_campaign=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off",
+ imageSrc = "https://img-getpocket.cdn.mozilla.net/direct?url=realUrl.png&resize=w618-h310",
+ sponsor = "Green Chef",
+ shim = ApiSpocShim(
+ click = "193815086ClickShim",
+ impression = "193815086ImpressionShim"
+ ),
+ priority = 3,
+ caps = ApiSpocCaps(
+ lifetimeCount = 50,
+ flightPeriod = 86400,
+ flightCount = 10,
+ ),
+ ),
+ ApiSpoc(
+ flightId = 191739667,
+ title = "This Leading Cash Back Card Is a Slam Dunk if You Want a One-Card Wallet",
+ url = "https://www.fool.com/the-ascent/credit-cards/landing/discover-it-cash-back-review-v2-csr/?utm_site=theascent&utm_campaign=ta-cc-co-pocket-discb-04012022-5-na-firefox&utm_medium=cpc&utm_source=pocket",
+ imageSrc = "https://img-getpocket.cdn.mozilla.net/direct?url=https%3A//s.zkcdn.net/Advertisers/359f56a5423c4926ab3aa148e448d839.webp&resize=w618-h310",
+ sponsor = "The Ascent",
+ shim = ApiSpocShim(
+ click = "177986195ClickShim",
+ impression = "177986195ImpressionShim"
+ ),
+ priority = 2,
+ caps = ApiSpocCaps(
+ lifetimeCount = 50,
+ flightPeriod = 86400,
+ flightCount = 10,
+ ),
+ ),
+ ApiSpoc(
+ flightId = 189212196,
+ title = "The Incredible Lawn Hack That Can Make Your Neighbors Green With Envy Over Your Lawn",
+ url = "https://go.lawnbuddy.org/zf/50/7673?campaign=SUN_Pocket2022&creative=SUN_LawnCompare4-TheIncredibleLawnHackThatCanMakeYourNeighborsGreenWithEnvyOverYourLawn-WithoutSpendingAFortuneOnNewGrassAndWithoutBreakingASweat-20220420",
+ imageSrc = "https://img-getpocket.cdn.mozilla.net/direct?url=https%3A//s.zkcdn.net/Advertisers/ce16302e184342cda0619c08b7604c9c.jpg&resize=w618-h310",
+ sponsor = "Sunday",
+ shim = ApiSpocShim(
+ click = "192560056ClickShim",
+ impression = "192560056ImpressionShim"
+ ),
+ priority = 1,
+ caps = ApiSpocCaps(
+ lifetimeCount = 50,
+ flightPeriod = 86400,
+ flightCount = 10,
+ ),
+ )
+ )
+
val dbExpectedPocketStory = PocketStoryEntity(
title = "How to Get Rid of Black Mold Naturally",
url = "https://getpocket.com/explore/item/how-to-get-rid-of-black-mold-naturally",
@@ -80,4 +142,18 @@ internal object PocketTestResources {
timeToRead = 11,
timesShown = 3
)
+
+ val dbExpectedPocketSpoc = SpocEntity(
+ id = 191739319,
+ url = "https://i.geistm.com/l/GC_7ReasonsKetoV2_Journiest?bcid=601c567ac5b18a0414cce1d4&bhid=624f3ea9adad7604086ac6b3&utm_content=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off_601c567ac5b18a0414cce1d4_624f3ea9adad7604086ac6b3&tv=su4&ct=NAT-PK-PROS-130OFF5WEEK-037&utm_medium=DB&utm_source=pocket~geistm&utm_campaign=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off",
+ title = "Eating Keto Has Never Been So Easy With Green Chef",
+ imageUrl = "https://img-getpocket.cdn.mozilla.net/direct?url=realUrl.png&resize=w618-h310",
+ sponsor = "Green Chef",
+ clickShim = "193815086ClickShim",
+ impressionShim = "193815086ImpressionShim",
+ priority = 3,
+ lifetimeCapCount = 50,
+ flightCapCount = 10,
+ flightCapPeriod = 86400,
+ )
}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsRepositoryTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsRepositoryTest.kt
new file mode 100644
index 00000000000..8e3dda6f02c
--- /dev/null
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsRepositoryTest.kt
@@ -0,0 +1,93 @@
+/* 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.service.pocket.spocs
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.service.pocket.ext.toLocalSpoc
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.service.pocket.spocs.db.SpocImpressionEntity
+import mozilla.components.service.pocket.spocs.db.SpocsDao
+import mozilla.components.support.test.argumentCaptor
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi // for runTest
+@RunWith(AndroidJUnit4::class)
+class SpocsRepositoryTest {
+ private val spocsRepo = spy(SpocsRepository(testContext))
+ private val dao = mock(SpocsDao::class.java)
+
+ @Before
+ fun setUp() {
+ doReturn(dao).`when`(spocsRepo).spocsDao
+ }
+
+ @Test
+ fun `GIVEN SpocsRepository WHEN asking for all spocs THEN return db entities mapped to domain type`() = runTest {
+ val spoc = PocketTestResources.dbExpectedPocketSpoc
+ val impressions = listOf(
+ SpocImpressionEntity(spoc.id),
+ SpocImpressionEntity(333),
+ SpocImpressionEntity(spoc.id),
+ )
+ doReturn(listOf(spoc)).`when`(dao).getAllSpocs()
+ doReturn(impressions).`when`(dao).getSpocsImpressions()
+
+ val result = spocsRepo.getAllSpocs()
+
+ verify(dao).getAllSpocs()
+ assertEquals(1, result.size)
+ assertSame(spoc.title, result[0].title)
+ assertSame(spoc.url, result[0].url)
+ assertSame(spoc.imageUrl, result[0].imageUrl)
+ assertSame(spoc.impressionShim, result[0].shim.impression)
+ assertSame(spoc.clickShim, result[0].shim.click)
+ assertEquals(spoc.priority, result[0].priority)
+ assertEquals(2, result[0].caps.currentImpressions.size)
+ assertEquals(spoc.lifetimeCapCount, result[0].caps.lifetimeCount)
+ assertEquals(spoc.flightCapCount, result[0].caps.flightCount)
+ assertEquals(spoc.flightCapPeriod, result[0].caps.flightPeriod)
+ }
+
+ @Test
+ fun `GIVEN SpocsRepository WHEN asking to delete all spocs THEN delete all from the database`() = runTest {
+ spocsRepo.deleteAllSpocs()
+
+ verify(dao).deleteAllSpocs()
+ }
+
+ @Test
+ fun `GIVEN SpocsRepository WHEN adding a new list of spocs THEN replace all present in the database`() = runTest {
+ val spoc = PocketTestResources.apiExpectedPocketSpocs[0]
+
+ spocsRepo.addSpocs(listOf(spoc))
+
+ verify(dao).cleanOldAndInsertNewSpocs(listOf(spoc.toLocalSpoc()))
+ }
+
+ @Test
+ fun `GIVEN SpocsRepository WHEN recording new spocs impressions THEN add this to the database`() = runTest {
+ val spocsIds = listOf(3, 33, 444)
+ val impressionsCaptor = argumentCaptor>()
+
+ spocsRepo.recordImpressions(spocsIds)
+
+ verify(dao).recordImpressions(impressionsCaptor.capture())
+ assertEquals(spocsIds.size, impressionsCaptor.value.size)
+ assertEquals(spocsIds[0], impressionsCaptor.value[0].spocId)
+ assertEquals(spocsIds[1], impressionsCaptor.value[1].spocId)
+ assertEquals(spocsIds[2], impressionsCaptor.value[2].spocId)
+ }
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsUseCasesTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsUseCasesTest.kt
new file mode 100644
index 00000000000..70f646b566b
--- /dev/null
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/SpocsUseCasesTest.kt
@@ -0,0 +1,304 @@
+/* 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.service.pocket.spocs
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.spocs.SpocsUseCases.RefreshSponsoredStories
+import mozilla.components.service.pocket.spocs.api.SpocsEndpoint
+import mozilla.components.service.pocket.stories.api.PocketResponse
+import mozilla.components.service.pocket.stories.api.PocketResponse.Failure
+import mozilla.components.service.pocket.stories.api.PocketResponse.Success
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+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.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import java.util.UUID
+import kotlin.reflect.KVisibility
+
+@OptIn(ExperimentalCoroutinesApi::class) // for runTest
+@RunWith(AndroidJUnit4::class)
+class SpocsUseCasesTest {
+ private val fetchClient: Client = mock()
+ private val profileId = UUID.randomUUID()
+ private val appId = "test"
+ private val useCases = spy(SpocsUseCases(testContext, fetchClient, profileId, appId))
+ private val spocsProvider: SpocsEndpoint = mock()
+ private val spocsRepo: SpocsRepository = mock()
+
+ @Before
+ fun setup() {
+ doReturn(spocsProvider).`when`(useCases).getSpocsProvider(any(), any(), any())
+ doReturn(spocsRepo).`when`(useCases).getSpocsRepository(any())
+ }
+
+ @Test
+ fun `GIVEN a SpocsUseCases THEN its visibility is internal`() {
+ assertClassVisibility(SpocsUseCases::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a RefreshSponsoredStories THEN its visibility is internal`() {
+ assertClassVisibility(RefreshSponsoredStories::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a GetSponsoredStories THEN its visibility is internal`() {
+ assertClassVisibility(SpocsUseCases.GetSponsoredStories::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a DeleteProfile THEN its visibility is internal`() {
+ assertClassVisibility(SpocsUseCases.DeleteProfile::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN RefreshSponsoredStories is constructed THEN use the same parameters`() {
+ val refreshUseCase = useCases.refreshStories
+
+ assertSame(testContext, refreshUseCase.appContext)
+ assertSame(fetchClient, refreshUseCase.fetchClient)
+ assertSame(profileId, refreshUseCase.profileId)
+ assertSame(appId, refreshUseCase.appId)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN RefreshSponsoredStories is constructed separately THEN default to use the same parameters`() {
+ val refreshUseCase = useCases.RefreshSponsoredStories()
+
+ assertSame(testContext, refreshUseCase.appContext)
+ assertSame(fetchClient, refreshUseCase.fetchClient)
+ assertSame(profileId, refreshUseCase.profileId)
+ assertSame(appId, refreshUseCase.appId)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN RefreshSponsoredStories is constructed separately THEN allow using different parameters`() {
+ val context2: Context = mock()
+ val fetchClient2: Client = mock()
+ val profileId2 = UUID.randomUUID()
+ val appId2 = "test"
+
+ val refreshUseCase = useCases.RefreshSponsoredStories(context2, fetchClient2, profileId2, appId2)
+
+ assertSame(context2, refreshUseCase.appContext)
+ assertSame(fetchClient2, refreshUseCase.fetchClient)
+ assertSame(profileId2, refreshUseCase.profileId)
+ assertSame(appId2, refreshUseCase.appId)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN RefreshSponsoredStories is called THEN download stories from API and return early if unsuccessful response`() = runTest {
+ val refreshUseCase = useCases.RefreshSponsoredStories()
+ val unsuccessfulResponse = getFailedSponsoredStories()
+ doReturn(unsuccessfulResponse).`when`(spocsProvider).getSponsoredStories()
+
+ val result = refreshUseCase.invoke()
+
+ assertFalse(result)
+ verify(spocsProvider).getSponsoredStories()
+ verify(spocsRepo, never()).addSpocs(any())
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN RefreshSponsoredStories is called THEN download stories from API and save a successful response locally`() = runTest {
+ val refreshUseCase = useCases.RefreshSponsoredStories()
+ val successfulResponse = getSuccessfulSponsoredStories()
+ doReturn(successfulResponse).`when`(spocsProvider).getSponsoredStories()
+
+ val result = refreshUseCase.invoke()
+
+ assertTrue(result)
+ verify(spocsProvider).getSponsoredStories()
+ verify(spocsRepo).addSpocs((successfulResponse as Success).data)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN GetSponsoredStories is constructed THEN use the same parameters`() {
+ val sponsoredStoriesUseCase = useCases.getStories
+
+ assertSame(testContext, sponsoredStoriesUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN GetSponsoredStories is constructed separately THEN default to use the same parameters`() {
+ val sponsoredStoriesUseCase = useCases.GetSponsoredStories()
+
+ assertSame(testContext, sponsoredStoriesUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN GetSponsoredStories is constructed separately THEN allow using different parameters`() {
+ val context2: Context = mock()
+
+ val sponsoredStoriesUseCase = useCases.GetSponsoredStories(context2)
+
+ assertSame(context2, sponsoredStoriesUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN GetSponsoredStories is called THEN return the stories from repository`() = runTest {
+ val sponsoredStoriesUseCase = useCases.GetSponsoredStories()
+ val stories = listOf(PocketTestResources.clientExpectedPocketStory)
+ doReturn(stories).`when`(spocsRepo).getAllSpocs()
+
+ val result = sponsoredStoriesUseCase.invoke()
+
+ verify(spocsRepo).getAllSpocs()
+ assertEquals(result, stories)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN GetSponsoredStories is called THEN return return an empty list if none are available in the repository`() = runTest {
+ val sponsoredStoriesUseCase = useCases.GetSponsoredStories()
+ doReturn(emptyList()).`when`(spocsRepo).getAllSpocs()
+
+ val result = sponsoredStoriesUseCase.invoke()
+
+ verify(spocsRepo).getAllSpocs()
+ assertTrue(result.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN RecordImpression is constructed THEN use the same parameters`() {
+ val recordImpressionsUseCase = useCases.getStories
+
+ assertSame(testContext, recordImpressionsUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN RecordImpression is constructed separately THEN default to use the same parameters`() {
+ val recordImpressionsUseCase = useCases.RecordImpression()
+
+ assertSame(testContext, recordImpressionsUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN RecordImpression is constructed separately THEN allow using different parameters`() {
+ val context2: Context = mock()
+
+ val recordImpressionsUseCase = useCases.RecordImpression(context2)
+
+ assertSame(context2, recordImpressionsUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN RecordImpression is called THEN record impressions in database`() = runTest {
+ val recordImpressionsUseCase = useCases.RecordImpression()
+ val storiesIds = listOf(5, 55, 4321)
+ val spocsIdsCaptor = argumentCaptor>()
+
+ recordImpressionsUseCase(storiesIds)
+
+ verify(spocsRepo).recordImpressions(spocsIdsCaptor.capture())
+ assertEquals(3, spocsIdsCaptor.value.size)
+ assertEquals(storiesIds[0], spocsIdsCaptor.value[0])
+ assertEquals(storiesIds[1], spocsIdsCaptor.value[1])
+ assertEquals(storiesIds[2], spocsIdsCaptor.value[2])
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN DeleteProfile is constructed THEN use the same parameters`() {
+ val deleteProfileUseCase = useCases.deleteProfile
+
+ assertSame(testContext, deleteProfileUseCase.context)
+ assertSame(fetchClient, deleteProfileUseCase.fetchClient)
+ assertSame(profileId, deleteProfileUseCase.profileId)
+ assertSame(appId, deleteProfileUseCase.appId)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN DeleteProfile is constructed separately THEN default to use the same parameters`() {
+ val deleteProfileUseCase = useCases.DeleteProfile()
+
+ assertSame(testContext, deleteProfileUseCase.context)
+ assertSame(fetchClient, deleteProfileUseCase.fetchClient)
+ assertSame(profileId, deleteProfileUseCase.profileId)
+ assertSame(appId, deleteProfileUseCase.appId)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases constructed WHEN DeleteProfile is constructed separately THEN allow using different parameters`() {
+ val context2: Context = mock()
+ val fetchClient2: Client = mock()
+ val profileId2 = UUID.randomUUID()
+ val appId2 = "test"
+
+ val deleteProfileUseCase = useCases.DeleteProfile(context2, fetchClient2, profileId2, appId2)
+
+ assertSame(context2, deleteProfileUseCase.context)
+ assertSame(fetchClient2, deleteProfileUseCase.fetchClient)
+ assertSame(profileId2, deleteProfileUseCase.profileId)
+ assertSame(appId2, deleteProfileUseCase.appId)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN DeleteProfile is called THEN return true if profile deletion was successful`() = runTest {
+ val deleteProfileUseCase = useCases.DeleteProfile()
+ val successfulResponse = Success(true)
+ doReturn(successfulResponse).`when`(spocsProvider).deleteProfile()
+
+ val result = deleteProfileUseCase.invoke()
+
+ verify(spocsProvider).deleteProfile()
+ assertTrue(result)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN DeleteProfile is called THEN return false if profile deletion was not successful`() = runTest {
+ val deleteProfileUseCase = useCases.DeleteProfile()
+ val unsuccessfulResponse = Failure()
+ doReturn(unsuccessfulResponse).`when`(spocsProvider).deleteProfile()
+
+ val result = deleteProfileUseCase.invoke()
+
+ verify(spocsProvider).deleteProfile()
+ assertFalse(result)
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN profile deletion is succesfull THEN delete all locally persisted spocs`() = runTest {
+ val deleteProfileUseCase = useCases.DeleteProfile()
+ val successfulResponse = Success(true)
+ doReturn(successfulResponse).`when`(spocsProvider).deleteProfile()
+
+ deleteProfileUseCase.invoke()
+
+ verify(spocsRepo).deleteAllSpocs()
+ }
+
+ @Test
+ fun `GIVEN SpocsUseCases WHEN profile deletion is not succesfull THEN keep all locally persisted spocs`() = runTest {
+ val deleteProfileUseCase = useCases.DeleteProfile()
+ val unsuccessfulResponse = Failure()
+ doReturn(unsuccessfulResponse).`when`(spocsProvider).deleteProfile()
+
+ deleteProfileUseCase.invoke()
+
+ verify(spocsRepo, never()).deleteAllSpocs()
+ }
+
+ private fun getSuccessfulSponsoredStories() =
+ PocketResponse.wrap(PocketTestResources.apiExpectedPocketSpocs)
+
+ private fun getFailedSponsoredStories() = PocketResponse.wrap(null)
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRawTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRawTest.kt
new file mode 100644
index 00000000000..0178819a142
--- /dev/null
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointRawTest.kt
@@ -0,0 +1,157 @@
+/* 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.service.pocket.spocs.api
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Response
+import mozilla.components.service.pocket.helpers.MockResponses
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.helpers.assertRequestParams
+import mozilla.components.service.pocket.helpers.assertResponseIsClosed
+import mozilla.components.service.pocket.helpers.assertSuccessfulRequestReturnsResponseBody
+import mozilla.components.service.pocket.stories.api.PocketEndpointRaw
+import mozilla.components.service.pocket.stories.api.PocketEndpointRaw.Companion
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import java.io.IOException
+import java.util.UUID
+import kotlin.reflect.KVisibility
+
+@RunWith(AndroidJUnit4::class)
+class SpocsEndpointRawTest {
+ private val profileId = UUID.randomUUID()
+ private val appId = "test"
+
+ private lateinit var endpoint: SpocsEndpointRaw
+ private lateinit var client: Client
+
+ private lateinit var errorResponse: Response
+ private lateinit var successResponse: Response
+ private lateinit var defaultResponse: Response
+
+ @Before
+ fun setUp() {
+ errorResponse = MockResponses.getError()
+ successResponse = MockResponses.getSuccess()
+ defaultResponse = errorResponse
+
+ client = mock().also {
+ doReturn(defaultResponse).`when`(it).fetch(any())
+ }
+
+ endpoint = SpocsEndpointRaw(client, profileId, appId)
+ }
+
+ @Test
+ fun `GIVEN a PocketEndpointRaw THEN its visibility is internal`() {
+ assertClassVisibility(PocketEndpointRaw::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `WHEN requesting spocs THEN the pocket proxy url is used`() {
+ val expectedUrl = "https://spocs.getpocket.dev/spocs"
+
+ assertRequestParams(
+ client,
+ makeRequest = {
+ endpoint.getSponsoredStories()
+ },
+ assertParams = { request ->
+ assertEquals(expectedUrl, request.url)
+ }
+ )
+ }
+
+ @Test
+ fun `WHEN requesting spocs and the client throws an IOException THEN null is returned`() {
+ doThrow(IOException::class.java).`when`(client).fetch(any())
+
+ assertNull(endpoint.getSponsoredStories())
+ }
+
+ @Test
+ fun `WHEN requesting spocs and the response is null THEN null is returned`() {
+ doReturn(null).`when`(client).fetch(any())
+
+ assertNull(endpoint.getSponsoredStories())
+ }
+
+ @Test
+ fun `WHEN requesting spocs and the response is not a success THEN null is returned`() {
+ doReturn(errorResponse).`when`(client).fetch(any())
+
+ assertNull(endpoint.getSponsoredStories())
+ }
+
+ @Test
+ fun `WHEN requesting profile deletion and the client throws an IOException THEN false is returned`() {
+ doThrow(IOException::class.java).`when`(client).fetch(any())
+
+ assertFalse(endpoint.deleteProfile())
+ }
+
+ @Test
+ fun `WHEN requesting account deletion and the response is not a success THEN false is returned`() {
+ doReturn(errorResponse).`when`(client).fetch(any())
+
+ assertFalse(endpoint.deleteProfile())
+ }
+
+ @Test
+ fun `WHEN requesting spocs and the response is a success THEN the response body is returned`() {
+ assertSuccessfulRequestReturnsResponseBody(client, endpoint::getSponsoredStories)
+ }
+
+ @Test
+ fun `WHEN requesting profile deletion and the response is a success THEN true is returned`() {
+ val response = MockResponses.getSuccess()
+ doReturn(response).`when`(client).fetch(any())
+
+ assertTrue(endpoint.deleteProfile())
+ }
+
+ @Test
+ fun `WHEN requesting spocs and the response is an error THEN response is closed`() {
+ assertResponseIsClosed(client, errorResponse) {
+ endpoint.getSponsoredStories()
+ }
+ }
+
+ @Test
+ fun `GIVEN a response from the request to delete profile WHEN inferring it's success THEN don't use the reponse body`() {
+ // Leverage the fact that a stream can only be read once to know if it was previously read.
+
+ doReturn(errorResponse).`when`(client).fetch(any())
+ errorResponse.use { "Only the response status should be used, not the response body" }
+
+ doReturn(successResponse).`when`(client).fetch(any())
+ successResponse.use { "Only the response status should be used, not the response body" }
+ }
+
+ @Test
+ fun `WHEN requesting spocs and the response is a success THEN response is closed`() {
+ assertResponseIsClosed(client, successResponse) {
+ endpoint.getSponsoredStories()
+ }
+ }
+
+ @Test
+ fun `WHEN newInstance is called THEN a new instance configured with the client provided is returned`() {
+ val result = Companion.newInstance(client)
+
+ assertSame(client, result.client)
+ }
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointTest.kt
new file mode 100644
index 00000000000..446fc13e95f
--- /dev/null
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsEndpointTest.kt
@@ -0,0 +1,158 @@
+/* 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.service.pocket.spocs.api
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.concept.fetch.Client
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.helpers.assertResponseIsFailure
+import mozilla.components.service.pocket.helpers.assertResponseIsSuccess
+import mozilla.components.service.pocket.stories.api.PocketResponse
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.util.UUID
+import kotlin.reflect.KVisibility
+
+@OptIn(ExperimentalCoroutinesApi::class) // for runTest
+@RunWith(AndroidJUnit4::class)
+class SpocsEndpointTest {
+
+ private lateinit var endpoint: SpocsEndpoint
+ private var raw: SpocsEndpointRaw = mock() // we shorten the name to avoid confusion with endpoint.
+ private var jsonParser: SpocsJSONParser = mock()
+ private var client: Client = mock()
+
+ @Before
+ fun setUp() {
+ endpoint = SpocsEndpoint(raw, jsonParser)
+ }
+
+ @Test
+ fun `GIVEN a SpocsEndpoint THEN its visibility is internal`() {
+ assertClassVisibility(SpocsEndpoint::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a request for spocs WHEN getting a null response THEN a failure is returned`() = runTest {
+ doReturn(null).`when`(raw).getSponsoredStories()
+
+ assertResponseIsFailure(endpoint.getSponsoredStories())
+ }
+
+ @Test
+ fun `GIVEN a request for spocs WHEN getting a null response THEN we do not attempt to parse stories`() = runTest {
+ doReturn(null).`when`(raw).getSponsoredStories()
+
+ doThrow(
+ AssertionError("JSONParser should not be called for a null endpoint response")
+ ).`when`(jsonParser).jsonToSpocs(any())
+
+ endpoint.getSponsoredStories()
+ }
+
+ @Test
+ fun `GIVEN a request for deleting profile WHEN the response is unsuccessful THEN a failure is returned`() = runTest {
+ doReturn(false).`when`(raw).deleteProfile()
+
+ assertResponseIsFailure(endpoint.deleteProfile())
+ }
+
+ @Test
+ fun `GIVEN a request for deleting profile WHEN the response is successful THEN success is returned`() = runTest {
+ doReturn(true).`when`(raw).deleteProfile()
+
+ assertResponseIsSuccess(endpoint.deleteProfile())
+ }
+
+ @Test
+ fun `GIVEN a request for spocs WHEN getting an empty response THEN a failure is returned`() = runTest {
+ arrayOf(
+ "",
+ " ",
+ ).forEach { response ->
+ doReturn(response).`when`(raw).getSponsoredStories()
+
+ assertResponseIsFailure(endpoint.getSponsoredStories())
+ }
+ }
+
+ @Test
+ fun `GIVEN a request for spocs WHEN getting an empty response THEN we do not attempt to parse stories`() = runTest {
+ arrayOf(
+ "",
+ " ",
+ ).forEach { response ->
+ doReturn(response).`when`(raw).getSponsoredStories()
+ doThrow(
+ AssertionError("JSONParser should not be called for an empty endpoint response")
+ ).`when`(jsonParser).jsonToSpocs(any())
+
+ endpoint.getSponsoredStories()
+ }
+ }
+
+ @Test
+ fun `GIVEN a request for stories WHEN getting a response THEN parse it through PocketJSONParser`() = runTest {
+ arrayOf(
+ "{}",
+ """{"expectedJSON": 101}""",
+ """{ "spocs": [] }"""
+ ).forEach { response ->
+ doReturn(response).`when`(raw).getSponsoredStories()
+
+ endpoint.getSponsoredStories()
+
+ verify(jsonParser, times(1)).jsonToSpocs(response)
+ }
+ }
+
+ @Test
+ fun `GIVEN a request for stories WHEN getting a valid response THEN success is returned`() = runTest {
+ endpoint = SpocsEndpoint(raw, SpocsJSONParser)
+ val response = PocketTestResources.pocketEndpointThreeSpocsResponse
+ doReturn(response).`when`(raw).getSponsoredStories()
+
+ val result = endpoint.getSponsoredStories()
+
+ assertTrue(result is PocketResponse.Success)
+ }
+
+ @Test
+ fun `GIVEN a request for stories WHEN getting a valid response THEN a success response with parsed stories is returned`() = runTest {
+ endpoint = SpocsEndpoint(raw, SpocsJSONParser)
+ val response = PocketTestResources.pocketEndpointThreeSpocsResponse
+ doReturn(response).`when`(raw).getSponsoredStories()
+ val expected = PocketTestResources.apiExpectedPocketSpocs
+
+ val result = endpoint.getSponsoredStories()
+
+ assertEquals(expected, (result as? PocketResponse.Success)?.data)
+ }
+
+ @Test
+ fun `WHEN newInstance is called THEN a new SpocsEndpoint is returned as a wrapper over a configured SpocsEndpointRaw`() {
+ val profileId = UUID.randomUUID()
+ val appId = "test"
+
+ val result = SpocsEndpoint.Companion.newInstance(client, profileId, appId)
+
+ assertSame(client, result.rawEndpoint.client)
+ assertSame(profileId, result.rawEndpoint.profileId)
+ assertSame(appId, result.rawEndpoint.appId)
+ }
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParserTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParserTest.kt
new file mode 100644
index 00000000000..a49d9bd96e3
--- /dev/null
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/api/SpocsJSONParserTest.kt
@@ -0,0 +1,200 @@
+/* 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.service.pocket.spocs.api
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import org.json.JSONObject
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.reflect.KVisibility
+
+@RunWith(AndroidJUnit4::class)
+class SpocsJSONParserTest {
+ @Test
+ fun `GIVEN a SpocsJSONParser THEN its visibility is internal`() {
+ assertClassVisibility(SpocsJSONParser::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN SpocsJSONParser WHEN parsing spocs THEN ApiSpocs are returned`() {
+ val expectedSpocs = PocketTestResources.apiExpectedPocketSpocs
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val actualSpocs = SpocsJSONParser.jsonToSpocs(pocketJSON)
+
+ assertNotNull(actualSpocs)
+ assertEquals(3, actualSpocs!!.size)
+ assertEquals(expectedSpocs, actualSpocs)
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing titles THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(2) }
+ val pocketJsonWithMissingTitle = removeJsonFieldFromArrayIndex("title", 2, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing urls THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(1) }
+ val pocketJsonWithMissingTitle = removeJsonFieldFromArrayIndex("url", 1, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing image urls THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(0) }
+ val pocketJsonWithMissingTitle = removeJsonFieldFromArrayIndex("image_src", 0, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing sponsors THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(1) }
+ val pocketJsonWithMissingTitle = removeJsonFieldFromArrayIndex("sponsor", 1, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing click shims THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(2) }
+ val pocketJsonWithMissingTitle = removeShimFromSpoc("click", 2, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing impression shims THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingTitle = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(1) }
+ val pocketJsonWithMissingTitle = removeShimFromSpoc("impression", 1, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingTitle)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingTitle.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing priority THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingPriority = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(1) }
+ val pocketJsonWithMissingPriority = removeJsonFieldFromArrayIndex("priority", 1, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingPriority)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingPriority.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing a lifetime count cap THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingLifetimeCap = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(0) }
+ val pocketJsonWithMissingLifetimeCap = removeCapFromSpoc(JSON_SPOC_CAPS_LIFETIME_KEY, 0, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingLifetimeCap)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingLifetimeCap.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing a flight count cap THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingFlightCountCap = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(1) }
+ val pocketJsonWithMissingFlightCountCap = removeCapFromSpoc(JSON_SPOC_CAPS_FLIGHT_COUNT_KEY, 1, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingFlightCountCap)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingFlightCountCap.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs with missing a flight period cap THEN those entries are dropped`() {
+ val pocketJSON = PocketTestResources.pocketEndpointThreeSpocsResponse
+ val expectedSpocsIfMissingFlightPeriodCap = ArrayList(PocketTestResources.apiExpectedPocketSpocs)
+ .apply { removeAt(2) }
+ val pocketJsonWithMissingFlightPeriodCap = removeCapFromSpoc(JSON_SPOC_CAPS_FLIGHT_PERIOD_KEY, 2, pocketJSON)
+
+ val result = SpocsJSONParser.jsonToSpocs(pocketJsonWithMissingFlightPeriodCap)
+
+ assertEquals(2, result!!.size)
+ assertEquals(expectedSpocsIfMissingFlightPeriodCap.joinToString(), result.joinToString())
+ }
+
+ @Test
+ fun `WHEN parsing spocs for an invalid JSON String THEN null is returned`() {
+ assertNull(SpocsJSONParser.jsonToSpocs("{!!}}"))
+ }
+}
+
+private fun removeJsonFieldFromArrayIndex(fieldName: String, indexInArray: Int, json: String): String {
+ val obj = JSONObject(json)
+ val spocsJson = obj.getJSONArray(KEY_ARRAY_SPOCS)
+ spocsJson.getJSONObject(indexInArray).remove(fieldName)
+ return obj.toString()
+}
+
+private fun removeShimFromSpoc(shimName: String, spocIndex: Int, json: String): String {
+ val obj = JSONObject(json)
+ val spocsJson = obj.getJSONArray(KEY_ARRAY_SPOCS)
+ val spocJson = spocsJson.getJSONObject(spocIndex)
+ spocJson.getJSONObject(JSON_SPOC_SHIMS_KEY).remove(shimName)
+ return obj.toString()
+}
+
+private fun removeCapFromSpoc(cap: String, spocIndex: Int, json: String): String {
+ val obj = JSONObject(json)
+ val spocsJson = obj.getJSONArray(KEY_ARRAY_SPOCS)
+ val spocJson = spocsJson.getJSONObject(spocIndex)
+ val capsJSON = spocJson.getJSONObject(JSON_SPOC_CAPS_KEY)
+
+ if (cap == JSON_SPOC_CAPS_LIFETIME_KEY) {
+ capsJSON.remove(cap)
+ } else {
+ capsJSON.getJSONObject(JSON_SPOC_CAPS_FLIGHT_KEY).remove(cap)
+ }
+
+ return obj.toString()
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocEntityTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocEntityTest.kt
new file mode 100644
index 00000000000..f7dee014180
--- /dev/null
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocEntityTest.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.service.pocket.spocs.db
+
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import org.junit.Test
+import kotlin.reflect.KVisibility.INTERNAL
+
+class SpocEntityTest {
+ // This is the data type persisted locally. No need to be public
+ @Test
+ fun `GIVEN a spoc entity THEN it's visibility is internal`() {
+ assertClassVisibility(SpocEntity::class, INTERNAL)
+ }
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntityTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntityTest.kt
new file mode 100644
index 00000000000..07be6ab8e42
--- /dev/null
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocImpressionEntityTest.kt
@@ -0,0 +1,29 @@
+/* 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.service.pocket.spocs.db
+
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import kotlin.reflect.KVisibility.INTERNAL
+
+class SpocImpressionEntityTest {
+ // This is the data type persisted locally. No need to be public
+ @Test
+ fun `GIVEN a spoc entity THEN it's visibility is internal`() {
+ assertClassVisibility(SpocImpressionEntity::class, INTERNAL)
+ }
+
+ @Test
+ fun `WHEN a new impression is created THEN the timestamp should be seconds from Epoch`() {
+ val nowInSeconds = System.currentTimeMillis() / 1000
+ val impression = SpocImpressionEntity(2)
+
+ assertTrue(
+ LongRange(nowInSeconds - 5, nowInSeconds + 5)
+ .contains(impression.impressionDateInSeconds)
+ )
+ }
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocsDaoTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocsDaoTest.kt
new file mode 100644
index 00000000000..9f4e34d95cc
--- /dev/null
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/spocs/db/SpocsDaoTest.kt
@@ -0,0 +1,513 @@
+/* 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.service.pocket.spocs.db
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.service.pocket.helpers.PocketTestResources
+import mozilla.components.service.pocket.stories.db.PocketRecommendationsDatabase
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class SpocsDaoTest {
+ private lateinit var database: PocketRecommendationsDatabase
+ private lateinit var dao: SpocsDao
+ private lateinit var executor: ExecutorService
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Before
+ fun setUp() {
+ executor = Executors.newSingleThreadExecutor()
+ database = Room
+ .inMemoryDatabaseBuilder(testContext, PocketRecommendationsDatabase::class.java)
+ .allowMainThreadQueries()
+ .build()
+ dao = database.spocsDao()
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ executor.shutdown()
+ }
+
+ @Test
+ fun `GIVEN an empty table WHEN a story is inserted and then queried THEN return the same story`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+
+ dao.insertSpocs(listOf(story))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(story), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different id is tried to be inserted THEN add that to the table`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ id = 1
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory, story), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different url is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ title = "updated" + story.url
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different title is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ title = "updated" + story.title
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different image url is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ imageUrl = "updated" + story.imageUrl
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different sponsor is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ sponsor = "updated" + story.sponsor
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different click shim is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ clickShim = "updated" + story.clickShim
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different impression shim is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ impressionShim = "updated" + story.impressionShim
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with different priority is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ priority = 765
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with a different lifetime cap count is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ lifetimeCapCount = 123
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with a different flight count cap is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ flightCapCount = 999
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN a story already persisted WHEN another story with a different flight period cap is tried to be inserted THEN replace the existing`() = runTest {
+ val story = PocketTestResources.dbExpectedPocketSpoc
+ val newStory = story.copy(
+ flightCapPeriod = 1
+ )
+ dao.insertSpocs(listOf(story))
+
+ dao.insertSpocs(listOf(newStory))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(newStory), result)
+ }
+
+ @Test
+ fun `GIVEN no persisted storied WHEN asked to insert a list of stories THEN add them all to the table`() = runTest {
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
+ val story4 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 4)
+
+ dao.insertSpocs(listOf(story1, story2, story3, story4))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(story1, story2, story3, story4), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to delete them THEN remove all from the table`() = runTest {
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
+ val story4 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 4)
+ dao.insertSpocs(listOf(story1, story2, story3, story4))
+
+ dao.deleteAllSpocs()
+ val result = dao.getAllSpocs()
+
+ assertTrue(result.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to delete some THEN remove remove the ones already persisted`() = runTest {
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
+ val story4 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 4)
+ val story5 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 5)
+ dao.insertSpocs(listOf(story1, story2, story3, story4))
+
+ dao.deleteSpocs(listOf(story2, story3, story5))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(story1, story4), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN remove from table all stories not found in the new list`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
+ val story4 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 4)
+ dao.insertSpocs(listOf(story1, story2, story3, story4))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(story2, story4))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(story2, story4), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN update stories with new ids`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ id = story1.id * 234
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ // Order gets reversed because the original story is replaced and another one is added.
+ assertEquals(listOf(story2, updatedStory1), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only url changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ url = "updated" + story1.url
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only title changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ title = "updated" + story1.title
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only image url changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ imageUrl = "updated" + story1.imageUrl
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only sponsor changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ sponsor = "updated" + story1.sponsor
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only the click shim changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ clickShim = "updated" + story1.clickShim
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only the impression shim changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ impressionShim = "updated" + story1.impressionShim
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only priority changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ priority = 678
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only the lifetime count cap changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ lifetimeCapCount = 4322
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only the flight count cap changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ flightCapCount = 111111
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only the flight period cap changed`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val updatedStory1 = story1.copy(
+ flightCapPeriod = 7
+ )
+ dao.insertSpocs(listOf(story1, story2))
+
+ dao.cleanOldAndInsertNewSpocs(listOf(updatedStory1, story2))
+ val result = dao.getAllSpocs()
+
+ assertEquals(listOf(updatedStory1, story2), result)
+ }
+
+ @Test
+ fun `GIVEN no stories are persisted WHEN asked to record an impression THEN don't persist data and don't throw errors`() = runTest {
+ dao.recordImpression(6543321)
+
+ val result = dao.getSpocsImpressions()
+
+ assertTrue(result.isEmpty())
+ }
+
+ @Test
+ fun `GIVEN stories are persisted WHEN asked to record impressions for other stories also THEN persist impression only for existing stories`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
+ dao.insertSpocs(listOf(story1, story3))
+
+ dao.recordImpressions(
+ listOf(
+ SpocImpressionEntity(story1.id),
+ SpocImpressionEntity(story2.id),
+ SpocImpressionEntity(story3.id)
+ )
+ )
+ val result = dao.getSpocsImpressions()
+
+ assertEquals(2, result.size)
+ assertEquals(story1.id, result[0].spocId)
+ assertEquals(story3.id, result[1].spocId)
+ }
+
+ @Test
+ fun `GIVEN stories are persisted WHEN asked to record impressions for existing stories THEN persist the impressions`() = runTest {
+ setupDatabseForTransactions()
+ val story1 = PocketTestResources.dbExpectedPocketSpoc
+ val story2 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 2)
+ val story3 = PocketTestResources.dbExpectedPocketSpoc.copy(id = story1.id * 3)
+ dao.insertSpocs(listOf(story1, story2, story3))
+
+ dao.recordImpressions(
+ listOf(
+ SpocImpressionEntity(story1.id),
+ SpocImpressionEntity(story3.id)
+ )
+ )
+ val result = dao.getSpocsImpressions()
+
+ assertEquals(2, result.size)
+ assertEquals(story1.id, result[0].spocId)
+ assertEquals(story3.id, result[1].spocId)
+ }
+
+ /**
+ * Sets an executor to be used for database transactions.
+ * Needs to be used along with "runTest" to ensure waiting for transactions to finish but not hang tests.
+ */
+ private fun setupDatabseForTransactions() {
+ database = Room
+ .inMemoryDatabaseBuilder(testContext, PocketRecommendationsDatabase::class.java)
+ .setTransactionExecutor(executor)
+ .allowMainThreadQueries()
+ .build()
+ dao = database.spocsDao()
+ }
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepositoryTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepositoryTest.kt
index 3a080b17114..5479a4878d1 100644
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepositoryTest.kt
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketRecommendationsRepositoryTest.kt
@@ -5,12 +5,13 @@
package mozilla.components.service.pocket.stories
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.components.service.pocket.ext.toPartialTimeShownUpdate
+import mozilla.components.service.pocket.ext.toPocketLocalStory
+import mozilla.components.service.pocket.ext.toPocketRecommendedStory
import mozilla.components.service.pocket.helpers.PocketTestResources
import mozilla.components.service.pocket.stories.db.PocketRecommendationsDao
-import mozilla.components.service.pocket.stories.ext.toPartialTimeShownUpdate
-import mozilla.components.service.pocket.stories.ext.toPocketLocalStory
-import mozilla.components.service.pocket.stories.ext.toPocketRecommendedStory
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
@@ -21,6 +22,7 @@ import org.mockito.Mockito.mock
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class PocketRecommendationsRepositoryTest {
@@ -35,7 +37,7 @@ class PocketRecommendationsRepositoryTest {
@Test
fun `GIVEN PocketRecommendationsRepository WHEN getPocketRecommendedStories is called THEN return db entities mapped to domain type`() {
- runBlocking {
+ runTest {
val dbStory = PocketTestResources.dbExpectedPocketStory
`when`(dao.getPocketStories()).thenReturn(listOf(dbStory))
@@ -49,7 +51,7 @@ class PocketRecommendationsRepositoryTest {
@Test
fun `GIVEN PocketRecommendationsRepository WHEN addAllPocketApiStories is called THEN persist the received story to db`() {
- runBlocking {
+ runTest {
val apiStories = PocketTestResources.apiExpectedPocketStoriesRecommendations
val apiStoriesMappedForDb = apiStories.map { it.toPocketLocalStory() }
@@ -61,7 +63,7 @@ class PocketRecommendationsRepositoryTest {
@Test
fun `GIVEN PocketRecommendationsRepository WHEN updateShownPocketRecommendedStories should persist the received story to db`() {
- runBlocking {
+ runTest {
val clientStories = listOf(PocketTestResources.clientExpectedPocketStory)
val clientStoriesPartialUpdate = clientStories.map { it.toPartialTimeShownUpdate() }
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketStoriesUseCasesTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketStoriesUseCasesTest.kt
index aa6bf16089b..00621d91945 100644
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketStoriesUseCasesTest.kt
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/PocketStoriesUseCasesTest.kt
@@ -4,21 +4,21 @@
package mozilla.components.service.pocket.stories
+import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.fetch.Client
-import mozilla.components.service.pocket.PocketRecommendedStory
-import mozilla.components.service.pocket.api.PocketEndpoint
-import mozilla.components.service.pocket.api.PocketResponse
+import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
import mozilla.components.service.pocket.helpers.PocketTestResources
import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.stories.api.PocketEndpoint
+import mozilla.components.service.pocket.stories.api.PocketResponse
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
-import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
-import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -31,26 +31,23 @@ import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import kotlin.reflect.KVisibility
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class PocketStoriesUseCasesTest {
- private val usecases = spy(PocketStoriesUseCases())
+ private val fetchClient: Client = mock()
+ private val useCases = spy(PocketStoriesUseCases(testContext, fetchClient))
private val pocketRepo: PocketRecommendationsRepository = mock()
private val pocketEndoint: PocketEndpoint = mock()
@Before
fun setup() {
- doReturn(pocketEndoint).`when`(usecases).getPocketEndpoint(any())
- doReturn(pocketRepo).`when`(usecases).getPocketRepository(any())
- }
-
- @After
- fun cleanup() {
- PocketStoriesUseCases.reset()
+ doReturn(pocketEndoint).`when`(useCases).getPocketEndpoint(any())
+ doReturn(pocketRepo).`when`(useCases).getPocketRepository(any())
}
@Test
- fun `GIVEN a PocketStoriesUseCases THEN its visibility is public`() {
- assertClassVisibility(PocketStoriesUseCases::class, KVisibility.PUBLIC)
+ fun `GIVEN a PocketStoriesUseCases THEN its visibility is internal`() {
+ assertClassVisibility(PocketStoriesUseCases::class, KVisibility.INTERNAL)
}
@Test
@@ -67,94 +64,125 @@ class PocketStoriesUseCasesTest {
}
@Test
- fun `GIVEN an uninitialized PocketStoriesUseCases WHEN initialize is called THEN api key and client are set`() {
- val client: Client = mock()
-
- PocketStoriesUseCases.initialize(client)
+ fun `GIVEN PocketStoriesUseCases WHEN RefreshPocketStories is constructed THEN use the same parameters`() {
+ val refreshUseCase = useCases.refreshStories
- assertSame(client, PocketStoriesUseCases.fetchClient)
+ assertSame(testContext, refreshUseCase.appContext)
+ assertSame(fetchClient, refreshUseCase.fetchClient)
}
@Test
- fun `GIVEN an already initialized PocketStoriesUseCases WHEN initialize is called THEN the new api key and client overwrite the old values`() {
- PocketStoriesUseCases.fetchClient = mock()
- val newClient: Client = mock()
+ fun `GIVEN PocketStoriesUseCases constructed WHEN RefreshPocketStories is constructed separately THEN default to use the same parameters`() {
+ val refreshUseCase = useCases.RefreshPocketStories()
- PocketStoriesUseCases.initialize(newClient)
-
- assertSame(newClient, PocketStoriesUseCases.fetchClient)
+ assertSame(testContext, refreshUseCase.appContext)
+ assertSame(fetchClient, refreshUseCase.fetchClient)
}
@Test
- fun `GIVEN PocketStoriesUseCases WHEN reset is called THEN the api key and fetch client references are freed`() {
- PocketStoriesUseCases.fetchClient = mock()
+ fun `GIVEN PocketStoriesUseCases constructed WHEN RefreshPocketStories is constructed separately THEN allow using different parameters`() {
+ val context2: Context = mock()
+ val fetchClient2: Client = mock()
- PocketStoriesUseCases.reset()
+ val refreshUseCase = useCases.RefreshPocketStories(context2, fetchClient2)
- assertNull(PocketStoriesUseCases.fetchClient)
+ assertSame(context2, refreshUseCase.appContext)
+ assertSame(fetchClient2, refreshUseCase.fetchClient)
}
@Test
- fun `GIVEN PocketStoriesUseCases WHEN RefreshPocketStories is called THEN download stories from API and return early if unsuccessful response`() {
- PocketStoriesUseCases.initialize(mock())
- val refreshUsecase = spy(
- usecases.RefreshPocketStories(testContext)
- )
+ fun `GIVEN PocketStoriesUseCases WHEN RefreshPocketStories is called THEN download stories from API and return early if unsuccessful response`() = runTest {
+ val refreshUseCase = useCases.RefreshPocketStories()
val successfulResponse = getSuccessfulPocketStories()
doReturn(successfulResponse).`when`(pocketEndoint).getRecommendedStories()
- val result = runBlocking {
- refreshUsecase.invoke()
- }
+ val result = refreshUseCase.invoke()
assertTrue(result)
verify(pocketEndoint).getRecommendedStories()
- runBlocking {
- verify(pocketRepo).addAllPocketApiStories((successfulResponse as PocketResponse.Success).data)
- }
+ verify(pocketRepo).addAllPocketApiStories((successfulResponse as PocketResponse.Success).data)
}
@Test
- fun `GIVEN PocketStoriesUseCases WHEN RefreshPocketStories is called THEN download stories from API and save a successful response locally`() {
- PocketStoriesUseCases.initialize(mock())
- val refreshUsecase = spy(usecases.RefreshPocketStories(testContext))
+ fun `GIVEN PocketStoriesUseCases WHEN RefreshPocketStories is called THEN download stories from API and save a successful response locally`() = runTest {
+ val refreshUseCase = useCases.RefreshPocketStories()
val successfulResponse = getFailedPocketStories()
-
doReturn(successfulResponse).`when`(pocketEndoint).getRecommendedStories()
- val result = runBlocking {
- refreshUsecase.invoke()
- }
+ val result = refreshUseCase.invoke()
assertFalse(result)
verify(pocketEndoint).getRecommendedStories()
- runBlocking {
- verify(pocketRepo, never()).addAllPocketApiStories(any())
- }
+ verify(pocketRepo, never()).addAllPocketApiStories(any())
}
@Test
- fun `GIVEN PocketStoriesUseCases WHEN GetPocketStories is called THEN delegate the repository to return locally stored stories`() =
- runBlocking {
- val getStoriesUsecase = spy(usecases.GetPocketStories(testContext))
+ fun `GIVEN PocketStoriesUseCases WHEN GetPocketStories is constructed THEN use the same parameters`() {
+ val getStoriesUseCase = useCases.getStories
+
+ assertSame(testContext, getStoriesUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases constructed WHEN GetPocketStories is constructed separately THEN default to use the same parameters`() {
+ val getStoriesUseCase = useCases.GetPocketStories()
+
+ assertSame(testContext, getStoriesUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases constructed WHEN GetPocketStories is constructed separately THEN allow using different parameters`() {
+ val context2: Context = mock()
+ val getStoriesUseCase = useCases.GetPocketStories(context2)
+
+ assertSame(context2, getStoriesUseCase.context)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases WHEN GetPocketStories is called THEN delegate the repository to return locally stored stories`() =
+ runTest {
+ val getStoriesUseCase = useCases.GetPocketStories()
doReturn(emptyList()).`when`(pocketRepo)
.getPocketRecommendedStories()
- var result = getStoriesUsecase.invoke()
+ var result = getStoriesUseCase.invoke()
verify(pocketRepo).getPocketRecommendedStories()
assertTrue(result.isEmpty())
val stories = listOf(PocketTestResources.clientExpectedPocketStory)
doReturn(stories).`when`(pocketRepo).getPocketRecommendedStories()
- result = getStoriesUsecase.invoke()
+ result = getStoriesUseCase.invoke()
// getPocketRecommendedStories() should've been called 2 times. Once in the above check, once now.
verify(pocketRepo, times(2)).getPocketRecommendedStories()
assertEquals(result, stories)
}
@Test
- fun `GIVEN PocketStoriesUseCases WHEN UpdateStoriesTimesShown is called THEN delegate the repository to update the stories shown`() = runBlocking {
- val updateStoriesTimesShown = usecases.UpdateStoriesTimesShown(testContext)
+ fun `GIVEN PocketStoriesUseCases WHEN UpdateStoriesTimesShown is constructed THEN use the same parameters`() {
+ val updateStoriesTimesShown = useCases.updateTimesShown
+
+ assertSame(testContext, updateStoriesTimesShown.context)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases constructed WHEN UpdateStoriesTimesShown is constructed separately THEN default to use the same parameters`() {
+ val updateStoriesTimesShown = useCases.UpdateStoriesTimesShown()
+
+ assertSame(testContext, updateStoriesTimesShown.context)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases constructed WHEN UpdateStoriesTimesShown is constructed separately THEN allow using different parameters`() {
+ val context2: Context = mock()
+
+ val updateStoriesTimesShown = useCases.UpdateStoriesTimesShown(context2)
+
+ assertSame(context2, updateStoriesTimesShown.context)
+ }
+
+ @Test
+ fun `GIVEN PocketStoriesUseCases WHEN UpdateStoriesTimesShown is called THEN delegate the repository to update the stories shown`() = runTest {
+ val updateStoriesTimesShown = useCases.UpdateStoriesTimesShown()
val updatedStories: List = mock()
updateStoriesTimesShown.invoke(updatedStories)
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/api/PocketApiStoryTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketApiStoryTest.kt
similarity index 92%
rename from components/service/pocket/src/test/java/mozilla/components/service/pocket/api/PocketApiStoryTest.kt
rename to components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketApiStoryTest.kt
index 58796464fed..34960a83d1b 100644
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/api/PocketApiStoryTest.kt
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketApiStoryTest.kt
@@ -2,7 +2,7 @@
* 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.service.pocket.api
+package mozilla.components.service.pocket.stories.api
import mozilla.components.service.pocket.helpers.assertClassVisibility
import org.junit.Test
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/api/PocketEndpointRawTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketEndpointRawTest.kt
similarity index 98%
rename from components/service/pocket/src/test/java/mozilla/components/service/pocket/api/PocketEndpointRawTest.kt
rename to components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketEndpointRawTest.kt
index 6d0dfb3b1a3..af018085d7e 100644
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/api/PocketEndpointRawTest.kt
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketEndpointRawTest.kt
@@ -2,7 +2,7 @@
* 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.service.pocket.api
+package mozilla.components.service.pocket.stories.api
import androidx.core.net.toUri
import androidx.test.ext.junit.runners.AndroidJUnit4
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/api/PocketEndpointTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketEndpointTest.kt
similarity index 98%
rename from components/service/pocket/src/test/java/mozilla/components/service/pocket/api/PocketEndpointTest.kt
rename to components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketEndpointTest.kt
index 93d73c649d8..5b12f6b5f57 100644
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/api/PocketEndpointTest.kt
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketEndpointTest.kt
@@ -2,7 +2,7 @@
* 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.service.pocket.api
+package mozilla.components.service.pocket.stories.api
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.concept.fetch.Client
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/api/PocketJSONParserTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketJSONParserTest.kt
similarity index 97%
rename from components/service/pocket/src/test/java/mozilla/components/service/pocket/api/PocketJSONParserTest.kt
rename to components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketJSONParserTest.kt
index 8a3ddc401aa..d3adc894553 100644
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/api/PocketJSONParserTest.kt
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketJSONParserTest.kt
@@ -2,12 +2,12 @@
* 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.service.pocket.api
+package mozilla.components.service.pocket.stories.api
import androidx.test.ext.junit.runners.AndroidJUnit4
-import mozilla.components.service.pocket.api.PocketJSONParser.Companion.KEY_ARRAY_ITEMS
import mozilla.components.service.pocket.helpers.PocketTestResources
import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.stories.api.PocketJSONParser.Companion.KEY_ARRAY_ITEMS
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/api/PocketResponseTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketResponseTest.kt
similarity index 97%
rename from components/service/pocket/src/test/java/mozilla/components/service/pocket/api/PocketResponseTest.kt
rename to components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketResponseTest.kt
index a14db137afd..59972a3ea1e 100644
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/api/PocketResponseTest.kt
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/api/PocketResponseTest.kt
@@ -2,7 +2,7 @@
* 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.service.pocket.api
+package mozilla.components.service.pocket.stories.api
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDaoTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDaoTest.kt
index 0ad7a8d1a0d..6a9995a24ac 100644
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDaoTest.kt
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/db/PocketRecommendationsDaoTest.kt
@@ -10,8 +10,7 @@ import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.runTest
import mozilla.components.service.pocket.helpers.PocketTestResources
import org.junit.After
import org.junit.Assert.assertEquals
@@ -52,7 +51,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN an empty table WHEN a story is inserted and then queried THEN return the same story`() = runBlockingTest {
+ fun `GIVEN an empty table WHEN a story is inserted and then queried THEN return the same story`() = runTest {
val story = PocketTestResources.dbExpectedPocketStory
dao.insertPocketStories(listOf(story))
@@ -62,7 +61,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN a story already persisted WHEN another story with identical url is tried to be inserted THEN add that to the table`() = runBlockingTest {
+ fun `GIVEN a story already persisted WHEN another story with identical url is tried to be inserted THEN add that to the table`() = runTest {
val story = PocketTestResources.dbExpectedPocketStory
val newStory = story.copy(
url = "updated" + story.url
@@ -76,7 +75,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN a story with the same url exists WHEN another story with updated title is tried to be inserted THEN don't update the table`() = runBlockingTest {
+ fun `GIVEN a story with the same url exists WHEN another story with updated title is tried to be inserted THEN don't update the table`() = runTest {
val story = PocketTestResources.dbExpectedPocketStory
val updatedStory = story.copy(
title = "updated" + story.title
@@ -91,7 +90,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN a story with the same url exists WHEN another story with updated imageUrl is tried to be inserted THEN don't update the table`() = runBlockingTest {
+ fun `GIVEN a story with the same url exists WHEN another story with updated imageUrl is tried to be inserted THEN don't update the table`() = runTest {
val story = PocketTestResources.dbExpectedPocketStory
val updatedStory = story.copy(
imageUrl = "updated" + story.imageUrl
@@ -105,7 +104,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN a story with the same url exists WHEN another story with updated publisher is tried to be inserted THEN don't update the table`() = runBlockingTest {
+ fun `GIVEN a story with the same url exists WHEN another story with updated publisher is tried to be inserted THEN don't update the table`() = runTest {
val story = PocketTestResources.dbExpectedPocketStory
val updatedStory = story.copy(
publisher = "updated" + story.publisher
@@ -119,7 +118,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN a story with the same url exists WHEN another story with updated category is tried to be inserted THEN don't update the table`() = runBlockingTest {
+ fun `GIVEN a story with the same url exists WHEN another story with updated category is tried to be inserted THEN don't update the table`() = runTest {
val story = PocketTestResources.dbExpectedPocketStory
val updatedStory = story.copy(
category = "updated" + story.category
@@ -133,7 +132,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN a story with the same url exists WHEN another story with updated timeToRead is tried to be inserted THEN don't update the table`() = runBlockingTest {
+ fun `GIVEN a story with the same url exists WHEN another story with updated timeToRead is tried to be inserted THEN don't update the table`() = runTest {
val story = PocketTestResources.dbExpectedPocketStory
val updatedStory = story.copy(
timesShown = story.timesShown * 2
@@ -147,7 +146,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN a story with the same url exists WHEN another story with updated timesShown is tried to be inserted THEN don't update the table`() = runBlockingTest {
+ fun `GIVEN a story with the same url exists WHEN another story with updated timesShown is tried to be inserted THEN don't update the table`() = runTest {
val story = PocketTestResources.dbExpectedPocketStory
val updatedStory = story.copy(
timesShown = story.timesShown * 2
@@ -161,7 +160,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN stories already persisted WHEN asked to delete some THEN remove them from the table`() = runBlockingTest {
+ fun `GIVEN stories already persisted WHEN asked to delete some THEN remove them from the table`() = runTest {
val story1 = PocketTestResources.dbExpectedPocketStory
val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
val story3 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "3")
@@ -175,7 +174,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN stories already persisted WHEN asked to delete one not present in the table THEN don't update the table`() = runBlockingTest {
+ fun `GIVEN stories already persisted WHEN asked to delete one not present in the table THEN don't update the table`() = runTest {
val story1 = PocketTestResources.dbExpectedPocketStory
val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
val story3 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "3")
@@ -189,7 +188,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN stories already persisted WHEN asked to update timesShown for one THEN update only that story`() = runBlockingTest {
+ fun `GIVEN stories already persisted WHEN asked to update timesShown for one THEN update only that story`() = runTest {
val story1 = PocketTestResources.dbExpectedPocketStory
val story2 = PocketTestResources.dbExpectedPocketStory.copy(
url = story1.url + "2",
@@ -222,7 +221,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN stories already persisted WHEN asked to update timesShown for one not present in the table THEN don't update the table`() = runBlockingTest {
+ fun `GIVEN stories already persisted WHEN asked to update timesShown for one not present in the table THEN don't update the table`() = runTest {
val story1 = PocketTestResources.dbExpectedPocketStory
val story2 = PocketTestResources.dbExpectedPocketStory.copy(
url = story1.url + "2",
@@ -246,7 +245,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN remove from table all stories not found in the new list`() = runBlocking {
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN remove from table all stories not found in the new list`() = runTest {
setupDatabseForTransactions()
val story1 = PocketTestResources.dbExpectedPocketStory
val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
@@ -261,7 +260,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN update stories with new urls`() = runBlocking {
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN update stories with new urls`() = runTest {
setupDatabseForTransactions()
val story1 = PocketTestResources.dbExpectedPocketStory
val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
@@ -278,7 +277,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN update stories with new image urls`() = runBlocking {
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN update stories with new image urls`() = runTest {
setupDatabseForTransactions()
val story1 = PocketTestResources.dbExpectedPocketStory
val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
@@ -294,7 +293,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only title changed`() = runBlocking {
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only title changed`() = runTest {
setupDatabseForTransactions()
val story1 = PocketTestResources.dbExpectedPocketStory
val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
@@ -310,7 +309,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only publisher changed`() = runBlocking {
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only publisher changed`() = runTest {
setupDatabseForTransactions()
val story1 = PocketTestResources.dbExpectedPocketStory
val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
@@ -326,7 +325,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only category changed`() = runBlocking {
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only category changed`() = runTest {
setupDatabseForTransactions()
val story1 = PocketTestResources.dbExpectedPocketStory
val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
@@ -342,7 +341,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only timeToRead changed`() = runBlocking {
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only timeToRead changed`() = runTest {
setupDatabseForTransactions()
val story1 = PocketTestResources.dbExpectedPocketStory
val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
@@ -358,7 +357,7 @@ class PocketRecommendationsDaoTest {
}
@Test
- fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only timesShown changed`() = runBlocking {
+ fun `GIVEN stories already persisted WHEN asked to clean and insert new ones THEN don't update story if only timesShown changed`() = runTest {
setupDatabseForTransactions()
val story1 = PocketTestResources.dbExpectedPocketStory
val story2 = PocketTestResources.dbExpectedPocketStory.copy(url = story1.url + "2")
@@ -375,7 +374,7 @@ class PocketRecommendationsDaoTest {
/**
* Sets an executor to be used for database transactions.
- * Needs to be used along with "runBlockingTest" to ensure waiting for transactions to finish but not hang tests.
+ * Needs to be used along with "runTest" to ensure waiting for transactions to finish but not hang tests.
*/
private fun setupDatabseForTransactions() {
database = Room
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/update/RefreshPocketWorkerTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/update/RefreshPocketWorkerTest.kt
deleted file mode 100644
index 78f368f400d..00000000000
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/update/RefreshPocketWorkerTest.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-/* 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.service.pocket.stories.update
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import mozilla.components.service.pocket.helpers.assertClassVisibility
-import org.junit.Test
-import org.junit.runner.RunWith
-import kotlin.reflect.KVisibility
-
-@RunWith(AndroidJUnit4::class)
-class RefreshPocketWorkerTest {
-
- @Test
- fun `GIVEN a RefreshPocketWorker THEN its visibility is internal`() {
- assertClassVisibility(RefreshPocketWorker::class, KVisibility.INTERNAL)
- }
-}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/DeleteSpocsProfileWorkerTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/DeleteSpocsProfileWorkerTest.kt
new file mode 100644
index 00000000000..ab663655777
--- /dev/null
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/DeleteSpocsProfileWorkerTest.kt
@@ -0,0 +1,65 @@
+/* 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.service.pocket.update
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.ListenableWorker.Result
+import androidx.work.await
+import androidx.work.testing.TestListenableWorkerBuilder
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.service.pocket.GlobalDependencyProvider
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.spocs.SpocsUseCases
+import mozilla.components.service.pocket.spocs.SpocsUseCases.DeleteProfile
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import kotlin.reflect.KVisibility.INTERNAL
+
+@ExperimentalCoroutinesApi // for runTestOnMain
+@RunWith(AndroidJUnit4::class)
+class DeleteSpocsProfileWorkerTest {
+ @get:Rule
+ val mainCoroutineRule = MainCoroutineRule()
+
+ @Test
+ fun `GIVEN a DeleteSpocsProfileWorker THEN its visibility is internal`() {
+ assertClassVisibility(RefreshSpocsWorker::class, INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a DeleteSpocsProfileWorker WHEN profile deletion is successful THEN return success`() = runTestOnMain {
+ val useCases: SpocsUseCases = mock()
+ val deleteProfileUseCase: DeleteProfile = mock()
+ doReturn(true).`when`(deleteProfileUseCase).invoke()
+ doReturn(deleteProfileUseCase).`when`(useCases).deleteProfile
+ GlobalDependencyProvider.SponsoredStories.initialize(useCases)
+ val worker = TestListenableWorkerBuilder(testContext).build()
+
+ val result = worker.startWork().await()
+
+ assertEquals(Result.success(), result)
+ }
+
+ @Test
+ fun `GIVEN a DeleteSpocsProfileWorker WHEN profile deletion fails THEN work should be retried`() = runTestOnMain {
+ val useCases: SpocsUseCases = mock()
+ val deleteProfileUseCase: DeleteProfile = mock()
+ doReturn(false).`when`(deleteProfileUseCase).invoke()
+ doReturn(deleteProfileUseCase).`when`(useCases).deleteProfile
+ GlobalDependencyProvider.SponsoredStories.initialize(useCases)
+ val worker = TestListenableWorkerBuilder(testContext).build()
+
+ val result = worker.startWork().await()
+
+ assertEquals(Result.retry(), result)
+ }
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/update/PocketStoriesRefreshSchedulerTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/PocketStoriesRefreshSchedulerTest.kt
similarity index 96%
rename from components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/update/PocketStoriesRefreshSchedulerTest.kt
rename to components/service/pocket/src/test/java/mozilla/components/service/pocket/update/PocketStoriesRefreshSchedulerTest.kt
index c791a7370d6..62a57ddc06d 100644
--- a/components/service/pocket/src/test/java/mozilla/components/service/pocket/stories/update/PocketStoriesRefreshSchedulerTest.kt
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/PocketStoriesRefreshSchedulerTest.kt
@@ -2,7 +2,7 @@
* 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.service.pocket.stories.update
+package mozilla.components.service.pocket.update
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.work.ExistingPeriodicWorkPolicy
@@ -12,7 +12,7 @@ import androidx.work.WorkManager
import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
import mozilla.components.service.pocket.PocketStoriesConfig
import mozilla.components.service.pocket.helpers.assertClassVisibility
-import mozilla.components.service.pocket.stories.update.RefreshPocketWorker.Companion.REFRESH_WORK_TAG
+import mozilla.components.service.pocket.update.RefreshPocketWorker.Companion.REFRESH_WORK_TAG
import mozilla.components.support.base.worker.Frequency
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/RefreshPocketWorkerTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/RefreshPocketWorkerTest.kt
new file mode 100644
index 00000000000..3691fb76a5e
--- /dev/null
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/RefreshPocketWorkerTest.kt
@@ -0,0 +1,64 @@
+/* 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.service.pocket.update
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.ListenableWorker
+import androidx.work.await
+import androidx.work.testing.TestListenableWorkerBuilder
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.service.pocket.GlobalDependencyProvider
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.stories.PocketStoriesUseCases
+import mozilla.components.service.pocket.stories.PocketStoriesUseCases.RefreshPocketStories
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import kotlin.reflect.KVisibility
+
+@ExperimentalCoroutinesApi // for runTestOnMain
+@RunWith(AndroidJUnit4::class)
+class RefreshPocketWorkerTest {
+
+ @get:Rule
+ val mainCoroutineRule = MainCoroutineRule()
+
+ @Test
+ fun `GIVEN a RefreshPocketWorker THEN its visibility is internal`() {
+ assertClassVisibility(RefreshPocketWorker::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a RefreshPocketWorker WHEN stories are refreshed successfully THEN return success`() = runTestOnMain {
+ val useCases: PocketStoriesUseCases = mock()
+ val refreshStoriesUseCase: RefreshPocketStories = mock()
+ doReturn(true).`when`(refreshStoriesUseCase).invoke()
+ doReturn(refreshStoriesUseCase).`when`(useCases).refreshStories
+ GlobalDependencyProvider.RecommendedStories.initialize(useCases)
+ val worker = TestListenableWorkerBuilder(testContext).build()
+
+ val result = worker.startWork().await()
+ assertEquals(ListenableWorker.Result.success(), result)
+ }
+
+ @Test
+ fun `GIVEN a RefreshPocketWorker WHEN stories are could not be refreshed THEN work should be retried`() = runTestOnMain {
+ val useCases: PocketStoriesUseCases = mock()
+ val refreshStoriesUseCase: RefreshPocketStories = mock()
+ doReturn(false).`when`(refreshStoriesUseCase).invoke()
+ doReturn(refreshStoriesUseCase).`when`(useCases).refreshStories
+ GlobalDependencyProvider.RecommendedStories.initialize(useCases)
+ val worker = TestListenableWorkerBuilder(testContext).build()
+
+ val result = worker.startWork().await()
+ assertEquals(ListenableWorker.Result.retry(), result)
+ }
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/RefreshSpocsWorkerTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/RefreshSpocsWorkerTest.kt
new file mode 100644
index 00000000000..89ea044add2
--- /dev/null
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/RefreshSpocsWorkerTest.kt
@@ -0,0 +1,64 @@
+/* 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.service.pocket.update
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.ListenableWorker
+import androidx.work.await
+import androidx.work.testing.TestListenableWorkerBuilder
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.service.pocket.GlobalDependencyProvider
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.spocs.SpocsUseCases
+import mozilla.components.service.pocket.spocs.SpocsUseCases.RefreshSponsoredStories
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import kotlin.reflect.KVisibility.INTERNAL
+
+@ExperimentalCoroutinesApi // for runTestOnMain
+@RunWith(AndroidJUnit4::class)
+class RefreshSpocsWorkerTest {
+
+ @get:Rule
+ val mainCoroutineRule = MainCoroutineRule()
+
+ @Test
+ fun `GIVEN a RefreshSpocsWorker THEN its visibility is internal`() {
+ assertClassVisibility(RefreshSpocsWorker::class, INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a RefreshSpocsWorker WHEN stories are refreshed successfully THEN return success`() = runTestOnMain {
+ val useCases: SpocsUseCases = mock()
+ val refreshStoriesUseCase: RefreshSponsoredStories = mock()
+ doReturn(true).`when`(refreshStoriesUseCase).invoke()
+ doReturn(refreshStoriesUseCase).`when`(useCases).refreshStories
+ GlobalDependencyProvider.SponsoredStories.initialize(useCases)
+ val worker = TestListenableWorkerBuilder(testContext).build()
+
+ val result = worker.startWork().await()
+ assertEquals(ListenableWorker.Result.success(), result)
+ }
+
+ @Test
+ fun `GIVEN a RefreshSpocsWorker WHEN stories are could not be refreshed THEN work should be retried`() = runTestOnMain {
+ val useCases: SpocsUseCases = mock()
+ val refreshStoriesUseCase: RefreshSponsoredStories = mock()
+ doReturn(false).`when`(refreshStoriesUseCase).invoke()
+ doReturn(refreshStoriesUseCase).`when`(useCases).refreshStories
+ GlobalDependencyProvider.SponsoredStories.initialize(useCases)
+ val worker = TestListenableWorkerBuilder(testContext).build()
+
+ val result = worker.startWork().await()
+ assertEquals(ListenableWorker.Result.retry(), result)
+ }
+}
diff --git a/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/SpocsRefreshSchedulerTest.kt b/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/SpocsRefreshSchedulerTest.kt
new file mode 100644
index 00000000000..f828c1a2bef
--- /dev/null
+++ b/components/service/pocket/src/test/java/mozilla/components/service/pocket/update/SpocsRefreshSchedulerTest.kt
@@ -0,0 +1,159 @@
+/* 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.service.pocket.update
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.work.BackoffPolicy
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequest
+import androidx.work.PeriodicWorkRequest
+import androidx.work.WorkManager
+import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
+import mozilla.components.service.pocket.PocketStoriesConfig
+import mozilla.components.service.pocket.helpers.assertClassVisibility
+import mozilla.components.service.pocket.update.DeleteSpocsProfileWorker.Companion.DELETE_SPOCS_PROFILE_WORK_TAG
+import mozilla.components.service.pocket.update.RefreshSpocsWorker.Companion.REFRESH_SPOCS_WORK_TAG
+import mozilla.components.support.base.worker.Frequency
+import mozilla.components.support.test.any
+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
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import java.util.concurrent.TimeUnit
+import kotlin.reflect.KVisibility
+
+@RunWith(AndroidJUnit4::class)
+class SpocsRefreshSchedulerTest {
+ @Test
+ fun `GIVEN a spocs refresh scheduler THEN its visibility is internal`() {
+ assertClassVisibility(SpocsRefreshScheduler::class, KVisibility.INTERNAL)
+ }
+
+ @Test
+ fun `GIVEN a spocs refresh scheduler WHEN scheduling stories refresh THEN a RefreshPocketWorker is created and enqueued`() {
+ val client: HttpURLConnectionClient = mock()
+ val scheduler = spy(
+ SpocsRefreshScheduler(
+ PocketStoriesConfig(
+ client, Frequency(1, TimeUnit.HOURS)
+ )
+ )
+ )
+ val workManager = mock()
+ val worker = mock()
+ doReturn(workManager).`when`(scheduler).getWorkManager(any())
+ doReturn(worker).`when`(scheduler).createPeriodicRefreshWorkerRequest(any())
+
+ scheduler.schedulePeriodicRefreshes(testContext)
+
+ verify(workManager).enqueueUniquePeriodicWork(REFRESH_SPOCS_WORK_TAG, ExistingPeriodicWorkPolicy.KEEP, worker)
+ }
+
+ @Test
+ fun `GIVEN a spocs refresh scheduler WHEN stopping stories refresh THEN it should cancel all unfinished work`() {
+ val scheduler = spy(SpocsRefreshScheduler(mock()))
+ val workManager = mock()
+ doReturn(workManager).`when`(scheduler).getWorkManager(any())
+
+ scheduler.stopPeriodicRefreshes(testContext)
+
+ verify(workManager).cancelAllWorkByTag(REFRESH_SPOCS_WORK_TAG)
+ verify(workManager, Mockito.never()).cancelAllWork()
+ }
+
+ @Test
+ fun `GIVEN a spocs refresh scheduler WHEN scheduling profile deletion THEN a RefreshPocketWorker is created and enqueued`() {
+ val client: HttpURLConnectionClient = mock()
+ val scheduler = spy(
+ SpocsRefreshScheduler(
+ PocketStoriesConfig(
+ client, Frequency(1, TimeUnit.HOURS)
+ )
+ )
+ )
+ val workManager = mock()
+ val worker = mock()
+ doReturn(workManager).`when`(scheduler).getWorkManager(any())
+ doReturn(worker).`when`(scheduler).createOneTimeProfileDeletionWorkerRequest()
+
+ scheduler.scheduleProfileDeletion(testContext)
+
+ verify(workManager).enqueueUniqueWork(DELETE_SPOCS_PROFILE_WORK_TAG, ExistingWorkPolicy.KEEP, worker)
+ }
+
+ @Test
+ fun `GIVEN a spocs refresh scheduler WHEN cancelling profile deletion THEN it should cancel all unfinished work`() {
+ val scheduler = spy(SpocsRefreshScheduler(mock()))
+ val workManager = mock()
+ doReturn(workManager).`when`(scheduler).getWorkManager(any())
+
+ scheduler.stopProfileDeletion(testContext)
+
+ verify(workManager).cancelAllWorkByTag(DELETE_SPOCS_PROFILE_WORK_TAG)
+ verify(workManager, never()).cancelAllWork()
+ }
+
+ @Test
+ fun `GIVEN a spocs refresh scheduler WHEN creating a periodic worker THEN a properly configured PeriodicWorkRequest is returned`() {
+ val scheduler = spy(SpocsRefreshScheduler(mock()))
+
+ val result = scheduler.createPeriodicRefreshWorkerRequest(
+ Frequency(1, TimeUnit.HOURS)
+ )
+
+ verify(scheduler).getWorkerConstrains()
+ assertTrue(result.workSpec.intervalDuration == TimeUnit.HOURS.toMillis(1))
+ assertFalse(result.workSpec.constraints.requiresBatteryNotLow())
+ assertFalse(result.workSpec.constraints.requiresCharging())
+ assertFalse(result.workSpec.constraints.hasContentUriTriggers())
+ assertFalse(result.workSpec.constraints.requiresStorageNotLow())
+ assertFalse(result.workSpec.constraints.requiresDeviceIdle())
+ assertTrue(result.workSpec.constraints.requiredNetworkType == NetworkType.CONNECTED)
+ assertTrue(result.tags.contains(REFRESH_SPOCS_WORK_TAG))
+ }
+
+ @Test
+ fun `GIVEN a spocs refresh scheduler WHEN creating a one time worker THEN a properly configured OneTimeWorkRequest is returned`() {
+ val scheduler = spy(SpocsRefreshScheduler(mock()))
+
+ val result = scheduler.createOneTimeProfileDeletionWorkerRequest()
+
+ verify(scheduler).getWorkerConstrains()
+ assertEquals(0, result.workSpec.intervalDuration)
+ assertEquals(0, result.workSpec.initialDelay)
+ assertEquals(BackoffPolicy.EXPONENTIAL, result.workSpec.backoffPolicy)
+ assertFalse(result.workSpec.constraints.requiresBatteryNotLow())
+ assertFalse(result.workSpec.constraints.requiresCharging())
+ assertFalse(result.workSpec.constraints.hasContentUriTriggers())
+ assertFalse(result.workSpec.constraints.requiresStorageNotLow())
+ assertFalse(result.workSpec.constraints.requiresDeviceIdle())
+ assertTrue(result.workSpec.constraints.requiredNetworkType == NetworkType.CONNECTED)
+ assertTrue(result.tags.contains(DELETE_SPOCS_PROFILE_WORK_TAG))
+ }
+
+ @Test
+ fun `GIVEN a spocs refresh scheduler THEN Worker constraints should be to have Internet`() {
+ val scheduler = SpocsRefreshScheduler(mock())
+
+ val result = scheduler.getWorkerConstrains()
+
+ assertFalse(result.requiresBatteryNotLow())
+ assertFalse(result.requiresCharging())
+ assertFalse(result.hasContentUriTriggers())
+ assertFalse(result.requiresStorageNotLow())
+ assertFalse(result.requiresDeviceIdle())
+ assertTrue(result.requiredNetworkType == NetworkType.CONNECTED)
+ }
+}
diff --git a/components/service/pocket/src/test/resources/pocket/sponsored_stories_response.json b/components/service/pocket/src/test/resources/pocket/sponsored_stories_response.json
new file mode 100644
index 00000000000..45ca1e5b632
--- /dev/null
+++ b/components/service/pocket/src/test/resources/pocket/sponsored_stories_response.json
@@ -0,0 +1,98 @@
+{
+ "feature_flags": {
+ "spoc_v2": true,
+ "collections": false
+ },
+ "spocs": [
+ {
+ "id": 193815086,
+ "flight_id": 191739319,
+ "campaign_id": 1315172,
+ "title": "Eating Keto Has Never Been So Easy With Green Chef",
+ "url": "https://i.geistm.com/l/GC_7ReasonsKetoV2_Journiest?bcid=601c567ac5b18a0414cce1d4&bhid=624f3ea9adad7604086ac6b3&utm_content=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off_601c567ac5b18a0414cce1d4_624f3ea9adad7604086ac6b3&tv=su4&ct=NAT-PK-PROS-130OFF5WEEK-037&utm_medium=DB&utm_source=pocket~geistm&utm_campaign=PKT_A_7ReasonsKetoV2_Journiest_40702022_RawMeatballUGC_130Off",
+ "domain": "journiest.com",
+ "excerpt": "Get Green Chef's Special Spring Offer: ${'$'}130 off plus free shipping.",
+ "priority": 3,
+ "raw_image_src": "https://s.zkcdn.net/Advertisers/a3644de3c18948ffbd9aa43e8f9c7bf0.png",
+ "image_src": "https://img-getpocket.cdn.mozilla.net/direct?url=realUrl.png&resize=w618-h310",
+ "shim": {
+ "click": "193815086ClickShim",
+ "impression": "193815086ImpressionShim",
+ "delete": "193815086DeleteShim",
+ "save": "193815086SaveShim"
+ },
+ "caps": {
+ "lifetime": 50,
+ "campaign": {
+ "count": 10,
+ "period": 86400
+ },
+ "flight": {
+ "count": 10,
+ "period": 86400
+ }
+ },
+ "sponsor": "Green Chef"
+ },
+ {
+ "id": 177986195,
+ "flight_id": 191739667,
+ "campaign_id": 63548984,
+ "title": "This Leading Cash Back Card Is a Slam Dunk if You Want a One-Card Wallet",
+ "url": "https://www.fool.com/the-ascent/credit-cards/landing/discover-it-cash-back-review-v2-csr/?utm_site=theascent&utm_campaign=ta-cc-co-pocket-discb-04012022-5-na-firefox&utm_medium=cpc&utm_source=pocket",
+ "domain": "fool.com",
+ "excerpt": "Make 2022 your year for a one-card wallet.",
+ "priority": 2,
+ "raw_image_src": "https://s.zkcdn.net/Advertisers/359f56a5423c4926ab3aa148e448d839.webp",
+ "image_src": "https://img-getpocket.cdn.mozilla.net/direct?url=https%3A//s.zkcdn.net/Advertisers/359f56a5423c4926ab3aa148e448d839.webp&resize=w618-h310",
+ "shim": {
+ "click": "177986195ClickShim",
+ "impression": "177986195ImpressionShim",
+ "delete": "177986195DeleteShim",
+ "save": "177986195SaveShim"
+ },
+ "caps": {
+ "lifetime": 50,
+ "campaign": {
+ "count": 10,
+ "period": 86400
+ },
+ "flight": {
+ "count": 10,
+ "period": 86400
+ }
+ },
+ "sponsor": "The Ascent"
+ },
+ {
+ "id": 192560056,
+ "flight_id": 189212196,
+ "campaign_id": 65544139,
+ "title": "The Incredible Lawn Hack That Can Make Your Neighbors Green With Envy Over Your Lawn",
+ "url": "https://go.lawnbuddy.org/zf/50/7673?campaign=SUN_Pocket2022&creative=SUN_LawnCompare4-TheIncredibleLawnHackThatCanMakeYourNeighborsGreenWithEnvyOverYourLawn-WithoutSpendingAFortuneOnNewGrassAndWithoutBreakingASweat-20220420",
+ "domain": "go.lawnbuddy.org",
+ "excerpt": "Without spending a fortune on new grass and without breaking a sweat.",
+ "priority": 1,
+ "raw_image_src": "https://s.zkcdn.net/Advertisers/ce16302e184342cda0619c08b7604c9c.jpg",
+ "image_src": "https://img-getpocket.cdn.mozilla.net/direct?url=https%3A//s.zkcdn.net/Advertisers/ce16302e184342cda0619c08b7604c9c.jpg&resize=w618-h310",
+ "shim": {
+ "click": "192560056ClickShim",
+ "impression": "192560056ImpressionShim",
+ "delete": "192560056DeleteShim",
+ "save": "192560056SaveShim"
+ },
+ "caps": {
+ "lifetime": 50,
+ "campaign": {
+ "count": 10,
+ "period": 86400
+ },
+ "flight": {
+ "count": 10,
+ "period": 86400
+ }
+ },
+ "sponsor": "Sunday"
+ }
+ ]
+}
diff --git a/components/service/sync-autofill/build.gradle b/components/service/sync-autofill/build.gradle
index 87bb3bbf66a..db95a8a066a 100644
--- a/components/service/sync-autofill/build.gradle
+++ b/components/service/sync-autofill/build.gradle
@@ -32,6 +32,7 @@ dependencies {
api project(':lib-dataprotect')
implementation project(':support-utils')
+ implementation project(':support-ktx')
implementation Dependencies.kotlin_stdlib
diff --git a/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegate.kt b/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegate.kt
new file mode 100644
index 00000000000..c42e2bc7f0f
--- /dev/null
+++ b/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegate.kt
@@ -0,0 +1,51 @@
+/* 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.service.sync.autofill
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.storage.CreditCard
+import mozilla.components.concept.storage.CreditCardEntry
+import mozilla.components.concept.storage.CreditCardValidationDelegate
+import mozilla.components.concept.storage.CreditCardValidationDelegate.Result
+import mozilla.components.concept.storage.CreditCardsAddressesStorage
+
+/**
+ * A delegate that will check against the [CreditCardsAddressesStorage] to determine if a given
+ * [CreditCard] can be persisted and returns information about why it can or cannot.
+ *
+ * @param storage An instance of [CreditCardsAddressesStorage].
+ */
+class DefaultCreditCardValidationDelegate(
+ private val storage: Lazy
+) : CreditCardValidationDelegate {
+
+ private val coroutineContext by lazy { Dispatchers.IO }
+
+ override suspend fun shouldCreateOrUpdate(creditCard: CreditCardEntry): Result =
+ withContext(coroutineContext) {
+ val creditCards = storage.value.getAllCreditCards()
+
+ val foundCreditCard = if (creditCards.isEmpty()) {
+ // No credit cards exist in the storage -> create a new credit card
+ null
+ } else {
+ val crypto = storage.value.getCreditCardCrypto()
+ val key = crypto.getOrGenerateKey()
+
+ creditCards.find {
+ val cardNumber = crypto.decrypt(key, it.encryptedCardNumber)?.number
+
+ it.guid == creditCard.guid || cardNumber == creditCard.number
+ }
+ }
+
+ if (foundCreditCard == null) {
+ Result.CanBeCreated
+ } else {
+ Result.CanBeUpdated(foundCreditCard)
+ }
+ }
+}
diff --git a/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegate.kt b/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegate.kt
index 1f6f52b5dd0..735ec085ce9 100644
--- a/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegate.kt
+++ b/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegate.kt
@@ -4,16 +4,21 @@
package mozilla.components.service.sync.autofill
-import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import mozilla.components.concept.storage.Address
import mozilla.components.concept.storage.CreditCard
+import mozilla.components.concept.storage.CreditCardEntry
import mozilla.components.concept.storage.CreditCardNumber
+import mozilla.components.concept.storage.CreditCardValidationDelegate
import mozilla.components.concept.storage.CreditCardsAddressesStorage
import mozilla.components.concept.storage.CreditCardsAddressesStorageDelegate
+import mozilla.components.concept.storage.ManagedKey
+import mozilla.components.concept.storage.NewCreditCardFields
+import mozilla.components.concept.storage.UpdatableCreditCardFields
+import mozilla.components.support.ktx.kotlin.last4Digits
/**
* [CreditCardsAddressesStorageDelegate] implementation.
@@ -21,40 +26,80 @@ import mozilla.components.concept.storage.CreditCardsAddressesStorageDelegate
* @param storage The [CreditCardsAddressesStorage] used for looking up addresses and credit cards to autofill.
* @param scope [CoroutineScope] for long running operations. Defaults to using the [Dispatchers.IO].
* @param isCreditCardAutofillEnabled callback allowing to limit [storage] operations if autofill is disabled.
+ * @param validationDelegate The [DefaultCreditCardValidationDelegate] used to check if a credit card
+ * can be saved in [storage] and returns information about why it can or cannot
*/
class GeckoCreditCardsAddressesStorageDelegate(
private val storage: Lazy,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
- private val isCreditCardAutofillEnabled: () -> Boolean = { false }
+ private val validationDelegate: DefaultCreditCardValidationDelegate = DefaultCreditCardValidationDelegate(storage),
+ private val isCreditCardAutofillEnabled: () -> Boolean = { false },
+ private val isAddressAutofillEnabled: () -> Boolean = { false }
) : CreditCardsAddressesStorageDelegate {
- override suspend fun decrypt(encryptedCardNumber: CreditCardNumber.Encrypted): CreditCardNumber.Plaintext? {
+ override suspend fun getOrGenerateKey(): ManagedKey {
+ val crypto = storage.value.getCreditCardCrypto()
+ return crypto.getOrGenerateKey()
+ }
+
+ override suspend fun decrypt(
+ key: ManagedKey,
+ encryptedCardNumber: CreditCardNumber.Encrypted
+ ): CreditCardNumber.Plaintext? {
val crypto = storage.value.getCreditCardCrypto()
- val key = crypto.getOrGenerateKey()
return crypto.decrypt(key, encryptedCardNumber)
}
- override fun onAddressesFetch(): Deferred> {
- return scope.async {
+ override suspend fun onAddressesFetch(): List = withContext(scope.coroutineContext) {
+ if (!isAddressAutofillEnabled()) {
+ emptyList()
+ } else {
storage.value.getAllAddresses()
}
}
- override fun onAddressSave(address: Address) {
+ override suspend fun onAddressSave(address: Address) {
TODO("Not yet implemented")
}
- override fun onCreditCardsFetch(): Deferred> {
- if (isCreditCardAutofillEnabled().not()) {
- return CompletableDeferred(listOf())
+ override suspend fun onCreditCardsFetch(): List =
+ withContext(scope.coroutineContext) {
+ if (!isCreditCardAutofillEnabled()) {
+ emptyList()
+ } else {
+ storage.value.getAllCreditCards()
+ }
}
- return scope.async {
- storage.value.getAllCreditCards()
+ override suspend fun onCreditCardSave(creditCard: CreditCardEntry) {
+ scope.launch {
+ when (val result = validationDelegate.shouldCreateOrUpdate(creditCard)) {
+ is CreditCardValidationDelegate.Result.CanBeCreated -> {
+ storage.value.addCreditCard(
+ NewCreditCardFields(
+ billingName = creditCard.name,
+ plaintextCardNumber = CreditCardNumber.Plaintext(creditCard.number),
+ cardNumberLast4 = creditCard.number.last4Digits(),
+ expiryMonth = creditCard.expiryMonth.toLong(),
+ expiryYear = creditCard.expiryYear.toLong(),
+ cardType = creditCard.cardType
+ )
+ )
+ }
+ is CreditCardValidationDelegate.Result.CanBeUpdated -> {
+ storage.value.updateCreditCard(
+ guid = result.foundCreditCard.guid,
+ creditCardFields = UpdatableCreditCardFields(
+ billingName = creditCard.name,
+ cardNumber = CreditCardNumber.Plaintext(creditCard.number),
+ cardNumberLast4 = creditCard.number.last4Digits(),
+ expiryMonth = creditCard.expiryMonth.toLong(),
+ expiryYear = creditCard.expiryYear.toLong(),
+ cardType = creditCard.cardType
+ )
+ )
+ }
+ }
}
}
-
- override fun onCreditCardSave(creditCard: CreditCard) {
- TODO("Not yet implemented")
- }
}
diff --git a/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorageTest.kt b/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorageTest.kt
index 310d9297a4d..39eaaa3d170 100644
--- a/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorageTest.kt
+++ b/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCreditCardsAddressesStorageTest.kt
@@ -5,7 +5,8 @@
package mozilla.components.service.sync.autofill
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.storage.CreditCard
import mozilla.components.concept.storage.CreditCardNumber
import mozilla.components.concept.storage.NewCreditCardFields
@@ -24,25 +25,27 @@ import org.junit.runner.RunWith
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class AutofillCreditCardsAddressesStorageTest {
+
private lateinit var storage: AutofillCreditCardsAddressesStorage
private lateinit var securePrefs: SecureAbove22Preferences
@Before
- fun setup() = runBlocking {
+ fun setup() {
// forceInsecure is set in the tests because a keystore wouldn't be configured in the test environment.
securePrefs = SecureAbove22Preferences(testContext, "autofill", forceInsecure = true)
storage = AutofillCreditCardsAddressesStorage(testContext, lazy { securePrefs })
}
@After
- fun cleanup() = runBlocking {
+ fun cleanup() {
storage.close()
}
@Test
- fun `add credit card`() = runBlocking {
+ fun `add credit card`() = runTest {
val plaintextNumber = CreditCardNumber.Plaintext("4111111111111111")
val creditCardFields = NewCreditCardFields(
billingName = "Jon Doe",
@@ -72,7 +75,7 @@ class AutofillCreditCardsAddressesStorageTest {
}
@Test
- fun `get credit card`() = runBlocking {
+ fun `get credit card`() = runTest {
val plaintextNumber = CreditCardNumber.Plaintext("5500000000000004")
val creditCardFields = NewCreditCardFields(
billingName = "Jon Doe",
@@ -88,12 +91,12 @@ class AutofillCreditCardsAddressesStorageTest {
}
@Test
- fun `GIVEN a non-existent credit card guid WHEN getCreditCard is called THEN null is returned`() = runBlocking {
+ fun `GIVEN a non-existent credit card guid WHEN getCreditCard is called THEN null is returned`() = runTest {
assertNull(storage.getCreditCard("guid"))
}
@Test
- fun `get all credit cards`() = runBlocking {
+ fun `get all credit cards`() = runTest {
val plaintextNumber1 = CreditCardNumber.Plaintext("5500000000000004")
val creditCardFields1 = NewCreditCardFields(
billingName = "Jane Fields",
@@ -141,7 +144,7 @@ class AutofillCreditCardsAddressesStorageTest {
}
@Test
- fun `update credit card`() = runBlocking {
+ fun `update credit card`() = runTest {
val creditCardFields = NewCreditCardFields(
billingName = "Jon Doe",
plaintextCardNumber = CreditCardNumber.Plaintext("4111111111111111"),
@@ -199,7 +202,7 @@ class AutofillCreditCardsAddressesStorageTest {
}
@Test
- fun `delete credit card`() = runBlocking {
+ fun `delete credit card`() = runTest {
val creditCardFields = NewCreditCardFields(
billingName = "Jon Doe",
plaintextCardNumber = CreditCardNumber.Plaintext("30000000000004"),
@@ -219,7 +222,7 @@ class AutofillCreditCardsAddressesStorageTest {
}
@Test
- fun `add address`() = runBlocking {
+ fun `add address`() = runTest {
val addressFields = UpdatableAddressFields(
givenName = "John",
additionalName = "",
@@ -253,7 +256,7 @@ class AutofillCreditCardsAddressesStorageTest {
}
@Test
- fun `get address`() = runBlocking {
+ fun `get address`() = runTest {
val addressFields = UpdatableAddressFields(
givenName = "John",
additionalName = "",
@@ -274,12 +277,12 @@ class AutofillCreditCardsAddressesStorageTest {
}
@Test
- fun `GIVEN a non-existent address guid WHEN getAddress is called THEN null is returned`() = runBlocking {
+ fun `GIVEN a non-existent address guid WHEN getAddress is called THEN null is returned`() = runTest {
assertNull(storage.getAddress("guid"))
}
@Test
- fun `get all addresses`() = runBlocking {
+ fun `get all addresses`() = runTest {
val addressFields1 = UpdatableAddressFields(
givenName = "John",
additionalName = "",
@@ -337,7 +340,7 @@ class AutofillCreditCardsAddressesStorageTest {
}
@Test
- fun `update address`() = runBlocking {
+ fun `update address`() = runTest {
val addressFields = UpdatableAddressFields(
givenName = "John",
additionalName = "",
@@ -389,7 +392,7 @@ class AutofillCreditCardsAddressesStorageTest {
}
@Test
- fun `delete address`() = runBlocking {
+ fun `delete address`() = runTest {
val addressFields = UpdatableAddressFields(
givenName = "John",
additionalName = "",
@@ -415,7 +418,7 @@ class AutofillCreditCardsAddressesStorageTest {
}
@Test
- fun `WHEN warmUp method is called THEN the database connection is established`(): Unit = runBlocking {
+ fun `WHEN warmUp method is called THEN the database connection is established`(): Unit = runTest {
val storageSpy = spy(storage)
storageSpy.warmUp()
diff --git a/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCryptoTest.kt b/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCryptoTest.kt
index be7e8ab92e3..64bb0e139c3 100644
--- a/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCryptoTest.kt
+++ b/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/AutofillCryptoTest.kt
@@ -5,7 +5,8 @@
package mozilla.components.service.sync.autofill
import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import mozilla.components.concept.storage.CreditCardNumber
import mozilla.components.concept.storage.KeyGenerationReason
import mozilla.components.concept.storage.ManagedKey
@@ -21,8 +22,10 @@ import org.junit.runner.RunWith
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoInteractions
+@ExperimentalCoroutinesApi // for runTest
@RunWith(AndroidJUnit4::class)
class AutofillCryptoTest {
+
private lateinit var securePrefs: SecureAbove22Preferences
@Before
@@ -32,7 +35,7 @@ class AutofillCryptoTest {
}
@Test
- fun `get key - new`() = runBlocking {
+ fun `get key - new`() = runTest {
val storage = mock()
val crypto = AutofillCrypto(testContext, securePrefs, storage)
val key = crypto.getOrGenerateKey()
@@ -47,7 +50,7 @@ class AutofillCryptoTest {
}
@Test
- fun `get key - lost`() = runBlocking {
+ fun `get key - lost`() = runTest {
val storage = mock()
val crypto = AutofillCrypto(testContext, securePrefs, storage)
val key = crypto.getOrGenerateKey()
@@ -63,7 +66,7 @@ class AutofillCryptoTest {
}
@Test
- fun `get key - corrupted`() = runBlocking {
+ fun `get key - corrupted`() = runTest {
val storage = mock()
val crypto = AutofillCrypto(testContext, securePrefs, storage)
val key = crypto.getOrGenerateKey()
@@ -80,7 +83,7 @@ class AutofillCryptoTest {
}
@Test
- fun `get key - corrupted subtly`() = runBlocking {
+ fun `get key - corrupted subtly`() = runTest {
val storage = mock