diff options
Diffstat (limited to 'mobile/android/android-components/components/support')
14 files changed, 433 insertions, 20 deletions
diff --git a/mobile/android/android-components/components/support/base/build.gradle b/mobile/android/android-components/components/support/base/build.gradle index 76918b4c21..bc7bffa7e7 100644 --- a/mobile/android/android-components/components/support/base/build.gradle +++ b/mobile/android/android-components/components/support/base/build.gradle @@ -74,6 +74,7 @@ android { dependencies { implementation ComponentsDependencies.kotlin_coroutines + implementation ComponentsDependencies.androidx_core_ktx implementation ComponentsDependencies.androidx_lifecycle_viewmodel api project(":concept-base") diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/BuildVersionProvider.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/BuildVersionProvider.kt new file mode 100644 index 0000000000..406e851049 --- /dev/null +++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/BuildVersionProvider.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.support.base.android + +import android.os.Build + +/** + * This class provides information about the build version without exposing the android framework + * APIs directly, making it easier to test the code that depends on it. + */ +interface BuildVersionProvider { + + /** + * Returns the SDK_INT of the current build version. + */ + fun sdkInt(): Int + + companion object { + const val FOREGROUND_SERVICE_RESTRICTIONS_STARTING_VERSION = Build.VERSION_CODES.S + } +} + +/** + * @see BuildVersionProvider + */ +class DefaultBuildVersionProvider : BuildVersionProvider { + + override fun sdkInt(): Int = Build.VERSION.SDK_INT +} diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/PowerManagerInfoProvider.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/PowerManagerInfoProvider.kt new file mode 100644 index 0000000000..0d8a78efb3 --- /dev/null +++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/PowerManagerInfoProvider.kt @@ -0,0 +1,39 @@ +/* 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.support.base.android + +import android.content.Context +import android.os.Build +import android.os.PowerManager +import androidx.core.content.ContextCompat + +/** + * This class provides information about battery optimisations without exposing the android + * framework APIs directly, making it easier to test the code that depends on it. + */ +interface PowerManagerInfoProvider { + + /** + * Returns true if the user has disabled battery optimisations for the app. + */ + fun isIgnoringBatteryOptimizations(): Boolean +} + +/** + * @see PowerManagerInfoProvider + */ +class DefaultPowerManagerInfoProvider(private val context: Context) : PowerManagerInfoProvider { + + private val powerManager by lazy { + ContextCompat.getSystemService(context, PowerManager::class.java) + } + + override fun isIgnoringBatteryOptimizations(): Boolean = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + powerManager?.isIgnoringBatteryOptimizations(context.packageName) ?: false + } else { + true + } +} diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/ProcessInfoProvider.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/ProcessInfoProvider.kt new file mode 100644 index 0000000000..2890b8c4c4 --- /dev/null +++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/ProcessInfoProvider.kt @@ -0,0 +1,32 @@ +/* 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.support.base.android + +import android.app.ActivityManager + +/** + * This class provides information the running app process without exposing the android framework + * APIs directly, making easier to test the code that depends on it. + */ +interface ProcessInfoProvider { + + /** + * Returns true if the current app process is in the foreground. + */ + fun isForegroundImportance(): Boolean +} + +/** + * @see ProcessInfoProvider + */ +class DefaultProcessInfoProvider : ProcessInfoProvider { + + override fun isForegroundImportance(): Boolean { + val appProcessInfo = ActivityManager.RunningAppProcessInfo() + ActivityManager.getMyMemoryState(appProcessInfo) + + return appProcessInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + } +} diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/StartForegroundService.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/StartForegroundService.kt new file mode 100644 index 0000000000..9bfa946205 --- /dev/null +++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/android/StartForegroundService.kt @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.base.android + +/** + * This class is used to start a foreground service safely. For api levels >= 31. It will only + * start the service if the app is in the foreground to prevent throwing the + * ForegroundServiceStartNotAllowedException. + * + * @param processInfoProvider The provider to check if the app is in the foreground. + * @param buildVersionProvider The provider to get the sdk version. + */ +class StartForegroundService( + private val processInfoProvider: ProcessInfoProvider = DefaultProcessInfoProvider(), + private val buildVersionProvider: BuildVersionProvider = DefaultBuildVersionProvider(), + private val powerManagerInfoProvider: PowerManagerInfoProvider, +) { + + /** + * @see StartForegroundService + * + * @param func The function to run if the app is in the foreground to follow the foreground + * service restrictions for sdk version >= 31. For lower versions, the function will always run. + */ + operator fun invoke(func: () -> Unit): Boolean = + if (buildVersionProvider.sdkInt() >= BuildVersionProvider.FOREGROUND_SERVICE_RESTRICTIONS_STARTING_VERSION) { + if (powerManagerInfoProvider.isIgnoringBatteryOptimizations() || + processInfoProvider.isForegroundImportance() + ) { + func() + true + } else { + false + } + } else { + func() + true + } +} diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/UserInteractionHandler.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/UserInteractionHandler.kt index 25e16ffcce..f52b19b93c 100644 --- a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/UserInteractionHandler.kt +++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/UserInteractionHandler.kt @@ -19,6 +19,13 @@ interface UserInteractionHandler { fun onBackPressed(): Boolean /** + * Called when this [UserInteractionHandler] gets the option to handle the user pressing the forward key. + * + * Returns true if this [UserInteractionHandler] consumed the event and no other components need to be notified. + */ + fun onForwardPressed(): Boolean = false + + /** * In most cases, when the home button is pressed, we invoke this callback to inform the app that the user * is going to leave the app. * diff --git a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapper.kt b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapper.kt index a6d1e8709c..02a48abdab 100644 --- a/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapper.kt +++ b/mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapper.kt @@ -157,6 +157,24 @@ class ViewBoundFeatureWrapper<T : LifecycleAwareFeature>() { } /** + * Convenience method for invoking [UserInteractionHandler.onForwardPressed] on a wrapped + * [LifecycleAwareFeature] that implements [UserInteractionHandler]. Returns false if + * the [LifecycleAwareFeature] was cleared already. + */ + @Synchronized + fun onForwardPressed(): Boolean { + val feature = feature ?: return false + + if (feature !is UserInteractionHandler) { + throw IllegalAccessError( + "Feature does not implement ${UserInteractionHandler::class.java.simpleName} interface", + ) + } + + return feature.onForwardPressed() + } + + /** * Convenience method for invoking [ActivityResultHandler.onActivityResult] on a wrapped * [LifecycleAwareFeature] that implements [ActivityResultHandler]. Returns false if * the [LifecycleAwareFeature] was cleared already. diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/android/StartForegroundServiceTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/android/StartForegroundServiceTest.kt new file mode 100644 index 0000000000..b583ed055c --- /dev/null +++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/android/StartForegroundServiceTest.kt @@ -0,0 +1,98 @@ +/* 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.support.base.android + +import android.os.Build +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class StartForegroundServiceTest { + + @Test + fun `WHEN build version below S THEN start foreground service should return true regardless of foreground importance`() { + val tested = StartForegroundService( + FakeProcessInfoProvider(false), + FakeBuildVersionProvider(Build.VERSION_CODES.P), + FakePowerManagerInfoProvider(false), + ) + + var isInvoked = false + val actual = tested.invoke { + isInvoked = true + } + val expected = true + + assertEquals(expected, actual) + assertTrue(isInvoked) + } + + @Test + fun `WHEN build version is S and above and foreground importance is false THEN start foreground service should return false`() { + val tested = StartForegroundService( + FakeProcessInfoProvider(false), + FakeBuildVersionProvider(Build.VERSION_CODES.S), + FakePowerManagerInfoProvider(false), + ) + + var isInvoked = false + val actual = tested.invoke { + isInvoked = true + } + + assertFalse(actual) + assertFalse(isInvoked) + } + + @Test + fun `WHEN build version is S and above and foreground importance is true THEN start foreground service should return true`() { + val tested = StartForegroundService( + FakeProcessInfoProvider(true), + FakeBuildVersionProvider(Build.VERSION_CODES.S), + FakePowerManagerInfoProvider(false), + ) + + var isInvoked = false + val actual = tested.invoke { + isInvoked = true + } + + assertTrue(actual) + assertTrue(isInvoked) + } + + @Test + fun `WHEN build version is S, foreground importance is false and battery optimisations are disabled THEN start foreground service should return true`() { + val tested = StartForegroundService( + FakeProcessInfoProvider(false), + FakeBuildVersionProvider(Build.VERSION_CODES.S), + FakePowerManagerInfoProvider(true), + ) + + var isInvoked = true + val actual = tested.invoke { + isInvoked = true + } + + assertTrue(actual) + assertTrue(isInvoked) + } + + class FakeProcessInfoProvider(private val isForegroundImportance: Boolean) : + ProcessInfoProvider { + override fun isForegroundImportance(): Boolean = isForegroundImportance + } + + class FakeBuildVersionProvider(private val sdkInt: Int) : BuildVersionProvider { + override fun sdkInt(): Int = sdkInt + } + + class FakePowerManagerInfoProvider( + private val isIgnoringBatteryOptimizations: Boolean, + ) : PowerManagerInfoProvider { + override fun isIgnoringBatteryOptimizations(): Boolean = isIgnoringBatteryOptimizations + } +} diff --git a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapperTest.kt b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapperTest.kt index a07364acc2..73c71caec8 100644 --- a/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapperTest.kt +++ b/mobile/android/android-components/components/support/base/src/test/java/mozilla/components/support/base/feature/ViewBoundFeatureWrapperTest.kt @@ -61,6 +61,34 @@ class ViewBoundFeatureWrapperTest { } @Test + fun `Calling onForwardPressed on an empty wrapper returns false`() { + val wrapper = ViewBoundFeatureWrapper<MockFeature>() + assertFalse(wrapper.onForwardPressed()) + } + + @Test + fun `onForwardPressed is forwarded to feature`() { + val feature = MockFeatureWithUserInteractionHandler(onForwardPressed = true) + + val wrapper = ViewBoundFeatureWrapper( + feature = feature, + owner = MockedLifecycleOwner(MockedLifecycle(Lifecycle.State.CREATED)), + view = mock(), + ) + + assertTrue(wrapper.onForwardPressed()) + assertTrue(feature.onForwardPressedInvoked) + + assertFalse( + ViewBoundFeatureWrapper( + feature = MockFeatureWithUserInteractionHandler(onForwardPressed = false), + owner = MockedLifecycleOwner(MockedLifecycle(Lifecycle.State.CREATED)), + view = mock(), + ).onForwardPressed(), + ) + } + + @Test fun `Calling onActivityResult on an empty wrapper returns false`() { val wrapper = ViewBoundFeatureWrapper<MockFeature>() assertFalse(wrapper.onActivityResult(0, mock(), RESULT_OK)) @@ -350,6 +378,19 @@ class ViewBoundFeatureWrapperTest { wrapper.onBackPressed() } + @Test(expected = IllegalAccessError::class) + fun `onForwardPressed throws if feature does not implement ForwardHandler`() { + val feature = MockFeature() + + val wrapper = ViewBoundFeatureWrapper( + feature = feature, + owner = MockedLifecycleOwner(MockedLifecycle(Lifecycle.State.CREATED)), + view = mock(), + ) + + wrapper.onForwardPressed() + } + @Test fun `Setting a feature clears a previously existing feature`() { val feature = MockFeature() @@ -434,6 +475,7 @@ private open class MockFeature : LifecycleAwareFeature { private class MockFeatureWithUserInteractionHandler( private val onBackPressed: Boolean = false, + private val onForwardPressed: Boolean = false, ) : MockFeature(), UserInteractionHandler { var onBackPressedInvoked = false private set @@ -442,6 +484,14 @@ private class MockFeatureWithUserInteractionHandler( onBackPressedInvoked = true return onBackPressed } + + var onForwardPressedInvoked = false + private set + + override fun onForwardPressed(): Boolean { + onForwardPressedInvoked = true + return onForwardPressed + } } private class MockFeatureWithActivityResultHandler( diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/View.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/View.kt index 625eae2c96..1efbb4db83 100644 --- a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/View.kt +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/android/view/View.kt @@ -13,7 +13,6 @@ import android.view.ViewTreeObserver import android.view.inputmethod.InputMethodManager import androidx.annotation.MainThread import androidx.core.content.getSystemService -import androidx.core.view.ViewCompat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel @@ -25,13 +24,13 @@ import java.lang.ref.WeakReference * Is the horizontal layout direction of this view from Right to Left? */ val View.isRTL: Boolean - get() = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL + get() = layoutDirection == View.LAYOUT_DIRECTION_RTL /** * Is the horizontal layout direction of this view from Left to Right? */ val View.isLTR: Boolean - get() = layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR + get() = layoutDirection == View.LAYOUT_DIRECTION_LTR /** * Tries to focus this view and show the soft input window for it. diff --git a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt index abcd1a741c..8b20bfd1e5 100644 --- a/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt +++ b/mobile/android/android-components/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt @@ -307,7 +307,16 @@ fun String.sanitizeFileName(): String { file.name.replace("\\.\\.+".toRegex(), ".") } else { file.name.replace(".", "") - } + }.replaceEscapedCharacters() +} + +/** + * Replaces control characters from ASCII 0 to ASCII 19 with '_' so the file name is valid + * and is correctly displayed. + */ +private fun String.replaceEscapedCharacters(): String { + val controlCharactersRegex = "[\\x00-\\x13]".toRegex() + return replace(controlCharactersRegex, "_") } /** @@ -330,6 +339,15 @@ fun String.urlEncode(): String { } /** + * Decodes '%'-escaped octets in the given string using the UTF-8 scheme. + * Replaces invalid octets with the unicode replacement character + * ("\\uFFFD"). + * + * @see [Uri.decode] + */ +fun String.decode(): String = Uri.decode(this) + +/** * Returns the string if it's length is not higher than @param[maximumLength] or * a @param[replacement] string if String length is higher than @param[maximumLength] */ diff --git a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt index 89ade2aace..eaa9e78c9a 100644 --- a/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt +++ b/mobile/android/android-components/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt @@ -181,24 +181,80 @@ class StringTest { @Test fun sanitizeFileName() { - var file = "/../../../../../../../../../../directory/file.......txt" - val fileName = "file.txt" - - assertEquals(fileName, file.sanitizeFileName()) - - file = "/root/directory/file.txt" - - assertEquals(fileName, file.sanitizeFileName()) - - assertEquals("file", "file".sanitizeFileName()) - - assertEquals("file", "file..".sanitizeFileName()) - - assertEquals("file", "file.".sanitizeFileName()) + val testCases = listOf( + "/../../../../../../../../../../directory/file.......txt" to "file.txt", + "/root/directory/file.txt" to "file.txt", + "file" to "file", + "file.." to "file", + "file." to "file", + ".file" to "file", + "test.2020.12.01.txt" to "test.2020.12.01.txt", + "\u0000filename" to "_filename", + "file\u0001name" to "file_name", + "data\u0002stream" to "data_stream", + "end\u0003text" to "end_text", + "trans\u0004mission" to "trans_mission", + "query\u0005result" to "query_result", + "acknowledge\u0006signal" to "acknowledge_signal", + "bell\u0007sound" to "bell_sound", + "back\u0008space" to "back_space", + "horizontal\u0009tab" to "horizontal_tab", + "new\u000Aline" to "new_line", + "vertical\u000Btab" to "vertical_tab", + "form\u000Cfeed" to "form_feed", + "return\u000Dcarriage" to "return_carriage", + "shift\u000Eout" to "shift_out", + "shift\u000Fin" to "shift_in", + "escape\u0010data" to "escape_data", + "device\u0011control1" to "device_control1", + "device\u0012control2" to "device_control2", + "device\u0013control3" to "device_control3", + ) - assertEquals("file", ".file".sanitizeFileName()) + testCases.forEach { (raw, escaped) -> + assertEquals(escaped, raw.sanitizeFileName()) + } + } + + @Test + fun `WHEN a string contains utf 8 encoded characters decode decodes it`() { + // List of pairs of encoded strings and their expected decoded results + val testCases = listOf( + "hello%20world" to "hello world", + "wow%21amazing" to "wow!amazing", + "quote%22here%22" to "quote\"here\"", + "hash%23tag" to "hash#tag", + "save%24money" to "save\$money", + "100%25complete" to "100%complete", + "you%26me" to "you&me", + "it%27s%20easy" to "it's easy", + "open%28now%29" to "open(now)", + "star%2Ashine" to "star*shine", + "add%2Bmore" to "add+more", + "comma%2Cseparated" to "comma,separated", + "dash%2Dbetween" to "dash-between", + "end%2Eperiod" to "end.period", + "path%2Fto%2Ffile" to "path/to/file", + "time%3A12%3A00" to "time:12:00", + "wait%3Bplease" to "wait;please", + "less%3Cthan" to "less<than", + "equals%3Dsign" to "equals=sign", + "greater%3Ethan" to "greater>than", + "what%3Fwhere" to "what?where", + "email%40domain.com" to "email@domain.com", + "bracket%5Bopen%5D" to "bracket[open]", + "escape%5Cbackslash" to "escape\\backslash", + "bracket%5Dclose%5D" to "bracket]close]", + "high%5Efive" to "high^five", + "accent%60grave" to "accent`grave", + "brace%7Bopenclose%7D" to "brace{openclose}", + "pipe%7Csymbol" to "pipe|symbol", + "tilde%7Ewave" to "tilde~wave", + ) - assertEquals("test.2020.12.01.txt", "test.2020.12.01.txt".sanitizeFileName()) + testCases.forEach { (encoded, decoded) -> + assertEquals(decoded, encoded.decode()) + } } @Test diff --git a/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionSupport.kt b/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionSupport.kt index b4b78d6272..3945a2fd06 100644 --- a/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionSupport.kt +++ b/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionSupport.kt @@ -269,6 +269,10 @@ object WebExtensionSupport { store.dispatch(WebExtensionAction.UpdateWebExtensionEnabledAction(extension.id, true)) } + override fun onOptionalPermissionsChanged(extension: WebExtension) { + installedExtensions[extension.id] = extension + } + override fun onDisabled(extension: WebExtension) { installedExtensions[extension.id] = extension store.dispatch(WebExtensionAction.UpdateWebExtensionEnabledAction(extension.id, false)) diff --git a/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionSupportTest.kt b/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionSupportTest.kt index b4e45b3f55..ec61832cef 100644 --- a/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionSupportTest.kt +++ b/mobile/android/android-components/components/support/webextensions/src/test/java/mozilla/components/support/webextensions/WebExtensionSupportTest.kt @@ -598,6 +598,25 @@ class WebExtensionSupportTest { } @Test + fun `reacts to optional permissions for an extension being changed`() { + val store = spy(BrowserStore()) + val engine: Engine = mock() + val ext: WebExtension = mock() + whenever(ext.id).thenReturn("extensionId") + whenever(ext.url).thenReturn("url") + + val delegateCaptor = argumentCaptor<WebExtensionDelegate>() + WebExtensionSupport.initialize(engine, store) + verify(engine).registerWebExtensionDelegate(delegateCaptor.capture()) + + assertNull(WebExtensionSupport.installedExtensions[ext.id]) + + delegateCaptor.value.onOptionalPermissionsChanged(ext) + + assertEquals(ext, WebExtensionSupport.installedExtensions[ext.id]) + } + + @Test fun `observes store and registers handlers on new engine sessions`() { val tab = createTab(id = "1", url = "https://www.mozilla.org") val customTab = createCustomTab(id = "2", url = "https://www.mozilla.org", source = SessionState.Source.Internal.CustomTab) |