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.

+
    +
  • Ìre môkọt ire owuwa iweweek okisi lek òbeme-etip? Kpọk sa ofifi mgbọ.
  • +
  • Ìre òkakọt ìwọlọ ere akpatan̄ yi? Kpọ ntibi-ntet eyi okwukwut kwun̄.
  • +
  • Ìre firewall sà ìre proxy okibem okwukwut kwun̄? Onineen̄ eyi ìkatatge môkọt igbugbana iwọwọlọ olik etip.
  • +
  • Owu gwa okikpọk ikaan̄ ufialek? Chichini ogwu àdìmin njin-etip kwun̄.
  • +
+ ]]>
+ + + Ìkakọt ìtibi ìtet + + + +
  • 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ñosu Informó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ĝo Klopodi 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 retejo Tiu ĉ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 foille Chaidh 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 @@ -
  • გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ, ამ ხარვეზის შესახებ.
  • +
  • გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ ამ ხარვეზის შესახებ.
  • ]]>
    დაზიანებული შიგთავსის შეცდომა - გვერდის ჩვენება, რომლის ნახვასაც ცდილობთ, ვერ ხერხდება, მონაცემთა გადაცემისას აღმოჩენილი შეცდომის გამო

    + გვერდის ჩვენება, რომლის ნახვასაც ცდილობთ, ვერ ხერხდება მონაცემთა გადაცემისას აღმოჩენილი შეცდომის გამო

      -
    • გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ, ამ ხარვეზის შესახებ.
    • +
    • გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ ამ ხარვეზის შესახებ.
    ]]>
    შიგთავსი დაზიანდა - გვერდის ჩვენება, რომლის ნახვასაც ცდილობთ, ვერ ხერხდება, მონაცემთა გადაცემისას აღმოჩენილი შეცდომის გამო

    + გვერდის ჩვენება, რომლის ნახვასაც ცდილობთ, ვერ ხერხდება მონაცემთა გადაცემისას აღმოჩენილი შეცდომის გამო

      -
    • გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ, ამ ხარვეზის შესახებ.
    • +
    • გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ ამ ხარვეზის შესახებ.
    ]]>
    @@ -197,7 +197,7 @@ ფაილთან წვდომა უარყოფილია -
  • შესაძლოა წაშლილია, გადატანილია ან ფაილზე წვდომის უფლებები შეზღუდულია.
  • +
  • შესაძლოა წაშლილია, გადატანილია ან ფაილთან წვდომის უფლებები შეზღუდულია.
  • ]]>
    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) + کوں مخصوص کریندا ہے جہڑا عام طور تے ویب براؤز کرݨ دے علاوہ کجھ ٻئے مقاصد کیتے ورتیندے۔ تہاݙی حفاظت تے سلامتی کیتے براؤزر نے ارداس کوں منسوخ کر ݙتا ہے۔

    ]]>
    + + + کنکشن ریسٹ تھی ڳیا + + + کنکشن قائم کرݨ دے دوران نیٹ ورک لنک وچ رکاوٹ پیدا تھئی۔ براہ کرم، ولدا کوشش کرو۔

    +
      +
    • سائٹ عارضی طور پر غیر دستیاب یا کافی مصروف تھی سڳدی ہے۔ کجھ دیر بعد وت کوشش کرو۔
    • +
    • جے تساں کوئی وی ورقہ لوڈ نہوے کر سڳدے پئے ، تاں آپݨاں ڈیوائس ڈیٹا یا وائی-فائی کنیکشن دی پڑتال کرو۔
    • +
    ]]>
    + + + غیر محفوظ فائل قسم + + +
  • ویب سائٹ دے مالکاں کوں ایہ مسلے ݙسݨ کیتے رابطہ کرو۔
  • + ]]>
    + + + خراب مواد نقص + + + +
  • جہڑا ورقہ ݙیکھݨ چاہندے ہو، کائنی ݙکھایا ون٘ڄ سڳدا کیونجو ڈیٹا ترسیل وچ ہک خرابی دا سراغ لڳے۔
  • +
  • ویب سائٹ دے مالک کوں ایں مسئلے دا ݙسݨ کیتے انہاں نال رابطہ کرو۔
  • + + ]]>
    + + + مواد تباہ تھی ڳیا + + +
  • جہڑا ورقہ ݙیکھݨ چاہندے ہو، کائنی ݙکھایا ون٘ڄ سڳدا کیونجو ڈیٹا ترسیل وچ ہک خرابی دا سراغ لڳے۔
  • +
  • ویب سائٹ دے مالک کوں ایں مسئلے دا ݙسݨ کیتے انہاں نال رابطہ کرو۔
  • + + ]]>
    + + + مواد اینکوڈ کرݨ وچ خرابی + + +
  • جہڑا ورقہ ݙیکھݨ چاہندے ہو، کائنی ݙکھایا ون٘ڄ سڳدا کیونجو ایہ ہک غلط یا بغیر سہارا تھئی کمپریشن ورتیندے۔
  • +
  • ویب سائٹ دے مالک کوں ایں مسئلے دا ݙسݨ کیتے انہاں نال رابطہ کرو۔
  • + + ]]>
    + + + پتہ کائنی لبھا + + + ݙتے ہوئے پتے کیتے براؤزر ہوسٹ سرور کائنی لبھ سڳا۔

    +
      +
    • پتے وچ ایں طرحاں دیاں ٹائپنگ غلطیاں دی پڑتال کرو + ww.example.com پر لکھݨا ہائی + www.example.com.
    • +
    • جے تساں کہیں وی ورقے کوں لوڈ نہوے کر سڳدے تاں ڈیوائس ڈیٹا یا وائی فائی کنکشن دی پڑتال کرو
    • +
    + ]]>
    + + + کوئی انٹرنیٹ کنکشن کائنی + + اپݨے نیٹ ورک کنکشن دی پڑتال کرو یا کجھ لمحے بعد ولدا کوشش کرو۔ + + ولدا لوڈ کرو + + + غلط پتہ + + مہیا کیتا ڳیا پتہ سُن٘ڄاپُو فارمیٹ وچ کائنی۔ سوہݨا، غلطیاں کیتے لوکیشن پٹی دی پڑتال کرو تے ولدا کوشش کرو۔

    + ]]>
    + + پتہ ٹھیک کائنی + + + +
  • ویب پتے عمومی طور تے این٘ویں http://www.example.com/ لکھے ویندے ہن
  • +
  • یقینی بݨاؤ جو تساں فارورڈ سلیشیز (جین٘ویں /) ورتندے پئے ہو۔
  • + ]]>
    + + + نامعلوم پروٹوکول + + پتے وچ پروٹوکول (مثال دے طور تے wxyz://) جیکوں براؤز کائنی سُن٘ڄݨیندا، تہوں تاں براؤزر سائٹ کوں ٹھیک طرحاں کنکٹ کائنی کر سڳدا۔

    +
      +
    • بھلا تساں ملٹی میڈیا تے ٻیاں غیرعبارت خدمتاں تے رسائی دی کوشش کریندے پئے ہو؟ ٻیاں ضروریات کیتے سائٹ دی پڑتال کرو۔
    • +
    • براؤزر دے سُن٘ڄاݨݨ کنوں پہلے کجھ پروٹوکولاں کوں تریجھی پارٹی سافٹ ویئر یا پلگ اناں دی لوڑ ہوندی ہے۔
    • +
    + ]]>
    + + + فائل کائنی لبھی + + +
  • بھلا آئٹم دا ناں وٹایا ون٘ڄ سڳدا ہائی، ہٹا ون٘ڄ سڳدا ہائی، یا ٻئی جاء تے منتقل کیتا ون٘ڄ سڳدا ہائی؟
  • +
  • بھلا پتے وچ ہجیاں، وݙے حروف یا ٹائپ دی کوئی ٻئی غلطی ہے؟
  • +
  • بھلا تہاݙے کول مطلوبہ آئٹم تائیں رسائی دی کافی اجازت ہے؟
  • + + ]]>
    + + + فائل تائیں رسائی مسترد کر ݙتی ڳئی ہائی + + +
  • ایہ ہٹا یا ٹور ݙتا ڳیا ہوسی یا فائل اجازتاں رسائی کائناں تھیوݨ ݙیندیاں پیاں ہوسن
  • + + ]]>
    + + + پراکسی سرور کنکشن دا انکار کر ݙتے + + براؤزر پراکسی سرر ورتݨ کیتے کنفیگر تھیا ہویا ہے، پر پراکسی نے کنکشن دا انکار کر ݙتے۔

    +
      +
    • بھلا براؤزر دی پراکسی کنفیگریشن ٹھیک ہے؟ ترتیباں دی پڑتال کرو تے ولدا کوشش کرو۔
    • +
    • بھلا پراکسی سروس ایں نیٹ ورک کنوں اجازت ݙیندی ہے؟
    • +
    • اڄݨ وی مشکل وچ ہو؟ مدد کیتے نیٹ ورک ایڈمن یا انٹرنیٹ فراہم کرݨ آلے نال مشورہ کرو۔
    • +
    + ]]>
    + + + پراکسی سرور کائنی لبھا + + براؤزر پراکسی سرر ورتݨ کیتے کنفیگر تھیا ہویا ہے، پر پراکسی کائنی لبھ سڳی۔

    +
      +
    • بھلا براؤزر دی پراکسی کنفیگریشن ٹھیک ہے؟ ترتیباں دی پڑتال کرو تے ولدا کوشش کرو۔
    • +
    • بھلا ڈیوائس فعال نیٹ ورک نال کنکٹ تھئی ہوئی ہے؟
    • +
    • اڄݨ وی مشکل وچ ہو؟ مدد کیتے نیٹ ورک ایڈمن یا انٹرنیٹ فراہم کرݨ آلے نال مشورہ کرو۔
    • +
    + ]]>
    + + + مالویئر سائٹ مسئلہ + + + %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 @@
  • Эҳтимол сервер бо дархостҳои зиёд машғул аст ё ин ки муваққатан дастнорас аст? Баъдтар аз нав кӯшиш кунед.
  • Шумо метавонед, ки сомонаҳои дигарро бинед? Пайвасти шабакавии дастгоҳро санҷед.
  • Дастгоҳ ё шабакаи шумо бо девори оташ ё прокси муҳофизат шудааст? Танзимоти нодуруст метавонад ба дидани сомона халал расонад.
  • -
  • Ҳанӯз мушкилӣ мекашед? Барои кумак ба маъмури шабака ё провайдери интернети худ муроҷиат намоед.
  • +
  • Ҳоло ҳам мушкилӣ мекашед? Барои кумак ба маъмури шабака ё провайдери интернети худ муроҷиат намоед.
  • ]]>
    @@ -96,9 +96,9 @@ Браузер кӯшиши ҷустуҷӯи маводи дархостшударо қатъ кард. Сомона дархостро тавре равон мекунад, ки ҳеҷ гоҳ ба анҷом нарасад.

    +

    Браузер кӯшиши ҷустуҷӯи маводи дархостшударо қатъ кард. Сомона дархостро тавре равона мекунад, ки раванд ҳеҷ гоҳ ба анҷом намерасад.

      -
    • Оё шумо кукиҳоеро, ки ин сомона талаб мекунад, ғайрифаъол кардед ё бастед?
    • +
    • Шумо кукиҳоеро, ки ин сомона талаб мекунад, ғайрифаъол кардед ё бастед?
    • Агар қабули кукиҳои сомона мушкилиро ҳал накунад, ин эҳтимолан мушкилии танзимоти сервер мебошад, на дастгоҳи шумо.
    ]]>
    @@ -120,7 +120,7 @@ Нишонии дархостшуда портеро муайян кард (масалан, mozilla.org:80 барои порти 80 дар mozilla.org). Одатан, порт барои мақсадҳои ғайр аз тамошокунии сомонаҳо истифода мешавад. Браузер барои муҳофизат ва амнияти шумо дархостро бекор кард.

    +

    Нишонии дархостшуда портеро муайян кард (масалан, mozilla.org:80 барои порти 80 дар mozilla.org), ки одатан барои мақсадҳои ба ғайр аз тамошокунии сомонаҳо истифода мешавад. Браузер барои муҳофизат ва амнияти шумо дархостро бекор кард.

    ]]>
    @@ -231,8 +231,8 @@
  • Эҳтимол аст, ки номи мавод иваз карда шуд, ё ин ки мавод тоза карда шуд, ё ба ҷойи дигар гузошта шуд?
  • -
  • Оё дар нишонӣ хатои имлоӣ, ҳуруфчинӣ ё дигар хатои типографӣ вуҷуд дорад?
  • -
  • Оё шумо ба маводи дархостшуда иҷозати дастрасии кофӣ доред?
  • +
  • Дар нишонӣ ягон хатои имлоӣ, ҳуруфчинӣ ё дигар хатои типографӣ вуҷуд дорад?
  • +
  • Шумо ба маводи дархостшуда иҷозати дастрасии кофӣ доред?
  • ]]>
    @@ -251,9 +251,9 @@ Браузер барои истифодаи сервери прокси танзим карда шудааст, аммо прокси аз пайвастшавӣ даст кашид.

      -
    • Оё танзимоти прокси дар браузер дуруст аст? Танзимотро санҷед ва аз нав кӯшиш кунед.
    • -
    • Оё хидмати прокси барои пайвастшавӣ аз ин шабака иҷозат медиҳад?
    • -
    • Ҳанӯз мушкилӣ мекашед? Барои кумак ба маъмури шабака ё провайдери интернети худ муроҷиат кунед.
    • +
    • Танзимоти прокси дар браузер дуруст аст? Танзимотро санҷед ва аз нав кӯшиш кунед.
    • +
    • Хидмати прокси барои пайвастшавӣ аз ин шабака иҷозат медиҳад?
    • +
    • Ҳоло ҳам мушкилӣ мекашед? Барои кумак ба маъмури шабака ё провайдери интернети худ муроҷиат кунед.
    ]]>
    @@ -263,9 +263,9 @@ Браузер барои истифодаи сервери прокси танзим карда шудааст, аммо прокси ёфт нашуд.

      -
    • Оё танзимоти прокси дар браузер дуруст аст? Танзимотро санҷед ва аз нав кӯшиш кунед.
    • -
    • Оё дастгоҳ ба шабакаи фаъол пайваст аст?
    • -
    • Ҳанӯз мушкилӣ мекашед? Барои кумак ба маъмури шабака ё провайдери интернети худ муроҷиат кунед.
    • +
    • Танзимоти прокси дар браузер дуруст аст? Танзимотро санҷед ва аз нав кӯшиш кунед.
    • +
    • Дастгоҳ ба шабакаи фаъол пайваст аст?
    • +
    • Ҳоло ҳам мушкилӣ мекашед? Барои кумак ба маъмури шабака ё провайдери интернети худ муроҷиат кунед.
    ]]>
    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 + Borrar La 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 permisos Valoración @@ -97,7 +97,7 @@ Aconséyase - Entá nun se sofita + Entá nun ye compatible Entá 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 anovamientu Ríquense %1$d permisos nuevos @@ -141,17 +141,17 @@ Anovamientos de complementos - Comprobador de complementos sofitaos + Comprobador de complementos compatibles Hai un complementu nuevu disponible Hai 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$s La 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 correutamente Nun 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án Doplně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 iminig Ldi 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ónicu La 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ónicu Llamar 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 enlace Compartir 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ðsflipa Opna mynd í nýjum flipa @@ -23,7 +23,7 @@ Nýr flipi opnaður - Nýr einkaflipi opnaður + Nýr huliðsflipi opnaður Tengill 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 posa Completó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 fhosgladh Tha 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 yemmed Ifuyla ittwazedmen - Asider (%1$s) + Asader (%1$s) - Sider + Sader Sefsex 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 multimedia La 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