summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/support/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
commitd8bbc7858622b6d9c278469aab701ca0b609cddf (patch)
treeeff41dc61d9f714852212739e6b3738b82a2af87 /mobile/android/android-components/components/support/test
parentReleasing progress-linux version 125.0.3-1~progress7.99u1. (diff)
downloadfirefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz
firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/components/support/test')
-rw-r--r--mobile/android/android-components/components/support/test/README.md19
-rw-r--r--mobile/android/android-components/components/support/test/build.gradle58
-rw-r--r--mobile/android/android-components/components/support/test/lint.xml13
-rw-r--r--mobile/android/android-components/components/support/test/src/main/AndroidManifest.xml4
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Expect.kt22
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/KArgumentCaptor.kt44
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Matchers.kt60
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Mock.kt54
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ThrowProperty.kt19
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Context.kt19
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Job.kt15
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/KProperty.kt29
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/fakes/FakeClock.kt19
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/file/Resources.kt19
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Extensions.kt14
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Fragments.kt32
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Permissions.kt21
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/shadow/PixelCopyShadow.kt41
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/Helpers.kt57
-rw-r--r--mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/MainCoroutineRule.kt56
-rw-r--r--mobile/android/android-components/components/support/test/src/test/java/PermissionsTest.kt28
-rw-r--r--mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/ThrowPropertyTest.kt24
-rw-r--r--mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/file/ResourcesTest.kt17
-rw-r--r--mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/ExtensionsTest.kt23
-rw-r--r--mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/FragmentsTest.kt30
-rw-r--r--mobile/android/android-components/components/support/test/src/test/resources/example_file.txt1
-rw-r--r--mobile/android/android-components/components/support/test/src/test/resources/robolectric.properties1
27 files changed, 739 insertions, 0 deletions
diff --git a/mobile/android/android-components/components/support/test/README.md b/mobile/android/android-components/components/support/test/README.md
new file mode 100644
index 0000000000..38c8f1ec54
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/README.md
@@ -0,0 +1,19 @@
+# [Android Components](../../../README.md) > Support > Test
+
+A collection of helpers for testing components in local unit tests (`src/test`).
+
+## Usage
+
+### Setting up the dependency
+
+Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
+
+```Groovy
+implementation "org.mozilla.components:support-test:{latest-version}"
+```
+
+## License
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/
diff --git a/mobile/android/android-components/components/support/test/build.gradle b/mobile/android/android-components/components/support/test/build.gradle
new file mode 100644
index 0000000000..6111218d8c
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/build.gradle
@@ -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/. */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ defaultConfig {
+ minSdkVersion config.minSdkVersion
+ compileSdk config.compileSdkVersion
+ targetSdkVersion config.targetSdkVersion
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt')
+ }
+ }
+
+ lint {
+ // Disabled since this caused issues with Android Gradle Plugin 3.2.1+ (NullPointerException:InvalidPackageDetector)
+ tasks.lint.enabled = false
+
+ lintConfig file("lint.xml")
+ }
+
+ namespace 'mozilla.components.support.test'
+}
+
+dependencies {
+ implementation ComponentsDependencies.kotlin_coroutines
+ implementation ComponentsDependencies.kotlin_reflect
+
+ implementation ComponentsDependencies.androidx_test_junit
+ api ComponentsDependencies.testing_mockito
+ implementation ComponentsDependencies.testing_coroutines
+ implementation ComponentsDependencies.androidx_fragment
+ implementation (ComponentsDependencies.testing_robolectric) {
+ exclude group: 'org.apache.maven'
+ }
+ implementation project(':support-base')
+ testImplementation ComponentsDependencies.testing_maven_ant_tasks
+
+ implementation ComponentsDependencies.androidx_test_core
+
+ testImplementation ComponentsDependencies.androidx_core
+ testImplementation ComponentsDependencies.androidx_test_junit
+ testImplementation project(':support-ktx')
+}
+
+apply from: '../../../android-lint.gradle'
+apply from: '../../../publish.gradle'
+ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
+
diff --git a/mobile/android/android-components/components/support/test/lint.xml b/mobile/android/android-components/components/support/test/lint.xml
new file mode 100644
index 0000000000..3c7a84d2fb
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/lint.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<lint>
+ <issue id="InvalidPackage">
+ <ignore path="**/byte-buddy-agent-*.jar"/>
+ <ignore path="**/bcprov-*on-*.jar"/>
+ <ignore path="**/shadows-framework-*.jar"/>
+ <ignore path="**/xstream-*.jar"/>
+ <ignore path="**/resources-4.0-*.jar"/>
+ </issue>
+</lint>
diff --git a/mobile/android/android-components/components/support/test/src/main/AndroidManifest.xml b/mobile/android/android-components/components/support/test/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..41078a7325
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<manifest />
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Expect.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Expect.kt
new file mode 100644
index 0000000000..eae9878667
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Expect.kt
@@ -0,0 +1,22 @@
+/* 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.test
+
+import org.junit.Assert.fail
+import kotlin.reflect.KClass
+
+/**
+ * Expect [block] to throw an exception. Otherwise fail the test (junit).
+ */
+inline fun <reified T : Throwable> expectException(clazz: KClass<T>, block: () -> Unit) {
+ try {
+ block()
+ fail("Expected exception to be thrown: $clazz")
+ } catch (e: Throwable) {
+ if (e !is T) {
+ throw e
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/KArgumentCaptor.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/KArgumentCaptor.kt
new file mode 100644
index 0000000000..6b1b6bba52
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/KArgumentCaptor.kt
@@ -0,0 +1,44 @@
+/* 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.test
+
+import org.mockito.ArgumentCaptor
+import kotlin.reflect.KClass
+
+/**
+ * Creates a [KArgumentCaptor] for given type.
+ */
+inline fun <reified T : Any> argumentCaptor(): KArgumentCaptor<T> {
+ return KArgumentCaptor(ArgumentCaptor.forClass(T::class.java), T::class)
+}
+class KArgumentCaptor<out T : Any?>(
+ private val captor: ArgumentCaptor<T>,
+ private val tClass: KClass<*>,
+) {
+
+ /**
+ * The first captured value of the argument.
+ * @throws IndexOutOfBoundsException if the value is not available.
+ */
+ val value: T
+ get() = captor.value
+
+ val allValues: List<T>
+ get() = captor.allValues
+
+ @Suppress("UNCHECKED_CAST")
+ fun capture(): T {
+ return captor.capture() ?: castNull()
+ }
+}
+
+/**
+ * Uses a quirk in the bytecode generated by Kotlin
+ * to cast [null] to a non-null type.
+ *
+ * See https://youtrack.jetbrains.com/issue/KT-8135.
+ */
+@Suppress("UNCHECKED_CAST")
+private fun <T> castNull(): T = null as T
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Matchers.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Matchers.kt
new file mode 100644
index 0000000000..b8d8f259ec
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Matchers.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.support.test
+
+import org.mockito.AdditionalMatchers
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+
+/**
+ * Mockito matcher that matches <strong>anything</strong>, including nulls and varargs.
+ *
+ * (The version from Mockito doesn't work correctly with Kotlin code.)
+ */
+fun <T> any(): T {
+ Mockito.any<T>()
+ return uninitialized()
+}
+
+/**
+ * Mockito matcher that matches if the argument is the same as the provided value.
+ *
+ * (The version from Mockito doesn't work correctly with Kotlin code.)
+ */
+fun <T> eq(value: T): T {
+ return Mockito.eq(value) ?: value
+}
+
+/**
+ * Mockito matcher that matches if the argument is not the same as the provided value.
+ *
+ * (The version from Mockito doesn't work correctly with Kotlin code.)
+ */
+fun <T> not(value: T): T {
+ return AdditionalMatchers.not(value) ?: value
+}
+
+/**
+ * Mockito matcher that captures the passed argument.
+ *
+ * (The version from Mockito doesn't work correctly with Kotlin code.)
+ */
+fun <T> capture(value: ArgumentCaptor<T>): T {
+ value.capture()
+ return uninitialized()
+}
+
+/**
+ * Mockito matcher that matches <strong>anything</strong> as nullable.
+ *
+ * (The version from Mockito doesn't work correctly with Kotlin code.)
+ */
+inline fun <reified T> nullable(): T {
+ Mockito.nullable(T::class.java)
+ return uninitialized()
+}
+
+@Suppress("UNCHECKED_CAST")
+fun <T> uninitialized(): T = null as T
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Mock.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Mock.kt
new file mode 100644
index 0000000000..38943c7577
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/Mock.kt
@@ -0,0 +1,54 @@
+/* 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.test
+
+import android.view.MotionEvent
+import org.mockito.Mockito
+
+/**
+ * Dynamically create a mock object. This method is helpful when creating mocks of classes
+ * using generics.
+ *
+ * Optional @param setup will be called on the mock after init.
+ *
+ * Instead of:
+ * <code>val foo = Mockito.mock(....Class of Bar<Baz>?...)<code>
+ *
+ * You can just use:
+ * <code>val foo: Bar<Baz> = mock()</code>
+ */
+inline fun <reified T : Any> mock(noinline setup: (T.() -> Unit)? = null): T = Mockito.mock(T::class.java)!!
+ .apply { setup?.invoke(this) }
+
+/**
+ * Equivalent to [mock] but allows inline setup of suspending functions.
+ */
+suspend inline fun <reified T : Any> coMock(noinline setup: (suspend T.() -> Unit)? = null): T = Mockito.mock(T::class.java)!!
+ .apply { setup?.invoke(this) }
+
+/**
+ * Enables stubbing methods. Use it when you want the mock to return particular value when particular method is called.
+ *
+ * Alias for [Mockito.when ].
+ *
+ * Taken from [mockito-kotlin](https://github.com/nhaarman/mockito-kotlin/).
+ */
+@Suppress("NOTHING_TO_INLINE")
+inline fun <T> whenever(methodCall: T) = Mockito.`when`(methodCall)!!
+
+/**
+ * Creates a custom [MotionEvent] for testing. As of SDK 28 [MotionEvent]s can't be mocked anymore and need to be created
+ * through [MotionEvent.obtain].
+ */
+fun mockMotionEvent(
+ action: Int,
+ downTime: Long = System.currentTimeMillis(),
+ eventTime: Long = System.currentTimeMillis(),
+ x: Float = 0f,
+ y: Float = 0f,
+ metaState: Int = 0,
+): MotionEvent {
+ return MotionEvent.obtain(downTime, eventTime, action, x, y, metaState)
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ThrowProperty.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ThrowProperty.kt
new file mode 100644
index 0000000000..fede1bf584
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ThrowProperty.kt
@@ -0,0 +1,19 @@
+/* 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.test
+
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+/**
+ * A [ReadWriteProperty] implementation for creating stub properties.
+ */
+class ThrowProperty<T> : ReadWriteProperty<Any, T> {
+ override fun getValue(thisRef: Any, property: KProperty<*>): T =
+ throw UnsupportedOperationException("Cannot get $property")
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: T) =
+ throw UnsupportedOperationException("Cannot set $property")
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Context.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Context.kt
new file mode 100644
index 0000000000..94e070eda7
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Context.kt
@@ -0,0 +1,19 @@
+/* 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.test.ext
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.R
+import androidx.appcompat.view.ContextThemeWrapper
+import mozilla.components.support.test.robolectric.testContext
+
+/**
+ * `testContext` wrapped with AppCompat theme.
+ *
+ * Useful for views that uses theme attributes, for example.
+ */
+@VisibleForTesting val appCompatContext: Context
+ get() = ContextThemeWrapper(testContext, R.style.Theme_AppCompat)
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Job.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Job.kt
new file mode 100644
index 0000000000..a1f8f80062
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/Job.kt
@@ -0,0 +1,15 @@
+/* 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.test.ext
+
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.runBlocking
+
+/**
+ * Blocks the current thread until the job is complete.
+ */
+fun Job.joinBlocking() {
+ runBlocking { join() }
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/KProperty.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/KProperty.kt
new file mode 100644
index 0000000000..cf27945f54
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/ext/KProperty.kt
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.test.ext
+
+import kotlin.reflect.KProperty0
+import kotlin.reflect.jvm.isAccessible
+
+/**
+ * Returns true if a lazy property has been initialized, or if the property is not lazy.
+ *
+ * implementation inspired by https://stackoverflow.com/a/42536189
+ */
+val KProperty0<*>.isLazyInitialized: Boolean
+ get() {
+ // Prevent exception for accessing private getDelegate function.
+ val originalAccessLevel = isAccessible
+ isAccessible = true
+
+ val lazyDelegate = getDelegate()
+ require(lazyDelegate is Lazy<*>) { "Expected receiver property to be lazy" }
+
+ val isLazyInitialized = lazyDelegate.isInitialized()
+
+ // Reset access level.
+ isAccessible = originalAccessLevel
+ return isLazyInitialized
+ }
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/fakes/FakeClock.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/fakes/FakeClock.kt
new file mode 100644
index 0000000000..0411873234
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/fakes/FakeClock.kt
@@ -0,0 +1,19 @@
+/* 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.test.fakes
+
+/**
+ * A fake clock exposing a [time] property that can be changed at test runtime.
+ *
+ * Ideally a class needing time hides the calls to System.currentTimeMillis() behind a lambda
+ * that can it can invoke. In a test this lambda can be replaced by FakeClock()::time.
+ */
+class FakeClock(
+ var time: Long = 0,
+) {
+ fun advanceBy(time: Long) {
+ this.time += time
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/file/Resources.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/file/Resources.kt
new file mode 100644
index 0000000000..2d5e842731
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/file/Resources.kt
@@ -0,0 +1,19 @@
+/* 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.test.file
+
+import org.junit.Assert
+
+/**
+ * Loads a file from the resources folder and returns its content as a string object.
+ * @param path The path where the file is located
+ */
+fun Any.loadResourceAsString(path: String): String {
+ return javaClass.getResourceAsStream(path)!!.bufferedReader().use {
+ it.readText()
+ }.also {
+ Assert.assertNotNull(it)
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Extensions.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Extensions.kt
new file mode 100644
index 0000000000..3b01aabdcd
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Extensions.kt
@@ -0,0 +1,14 @@
+/* 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.test.robolectric
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+
+/**
+ * Provides application context for test purposes
+ */
+inline val testContext: Context
+ get() = ApplicationProvider.getApplicationContext()
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Fragments.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Fragments.kt
new file mode 100644
index 0000000000..6ad1d3d526
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Fragments.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.test.robolectric
+
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import org.robolectric.Robolectric
+
+/**
+ * Set up an added [Fragment] to a [FragmentActivity] that has been initialized to a resumed state.
+ *
+ * @param fragmentTag the name that will be used to tag the fragment inside the [FragmentManager].
+ * @param fragmentFactory a lambda function that returns a Fragment that will be added to the Activity.
+ *
+ * @return The same [Fragment] that was returned from [fragmentFactory] after it got added to the
+ * Activity.
+ */
+inline fun <T : Fragment> createAddedTestFragment(fragmentTag: String = "test", fragmentFactory: () -> T): T {
+ val activity = Robolectric.buildActivity(FragmentActivity::class.java)
+ .create()
+ .start()
+ .resume()
+ .get()
+
+ return fragmentFactory().also {
+ activity.supportFragmentManager.beginTransaction()
+ .add(it, fragmentTag)
+ .commitNow()
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Permissions.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Permissions.kt
new file mode 100644
index 0000000000..606956631a
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/Permissions.kt
@@ -0,0 +1,21 @@
+/* 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.test.robolectric
+
+import android.app.Application
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import org.robolectric.Shadows.shadowOf
+
+/**
+ * A helper for working with permission
+ * just pass one or more permission that you need to be granted.
+ * @param permissions list of permissions that you need to be granted.
+ */
+fun grantPermission(vararg permissions: String) {
+ val application = shadowOf(getApplicationContext<Application>())
+ permissions.map {
+ application.grantPermissions(it)
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/shadow/PixelCopyShadow.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/shadow/PixelCopyShadow.kt
new file mode 100644
index 0000000000..45b6c86d71
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/robolectric/shadow/PixelCopyShadow.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.test.robolectric.shadow
+
+import android.annotation.TargetApi
+import android.graphics.Bitmap
+import android.graphics.Rect
+import android.os.Build
+import android.os.Handler
+import android.view.PixelCopy
+import android.view.Window
+import org.robolectric.annotation.Implementation
+import org.robolectric.annotation.Implements
+
+/**
+ * Shadow for [PixelCopy] API.
+ */
+@Implements(PixelCopy::class, minSdk = Build.VERSION_CODES.N)
+@TargetApi(Build.VERSION_CODES.N)
+class PixelCopyShadow {
+
+ companion object {
+ var copyResult = PixelCopy.SUCCESS
+
+ @JvmStatic
+ @Implementation
+ // Some parameters are unused but method signature should be the same as for original class.
+ @Suppress("UNUSED_PARAMETER")
+ fun request(
+ source: Window,
+ srcRect: Rect?,
+ dest: Bitmap,
+ listener: PixelCopy.OnPixelCopyFinishedListener,
+ listenerThread: Handler?,
+ ) {
+ listener.onPixelCopyFinished(copyResult)
+ }
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/Helpers.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/Helpers.kt
new file mode 100644
index 0000000000..17b3bbf9e6
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/Helpers.kt
@@ -0,0 +1,57 @@
+/* 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/. */
+
+@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // for TestMainDispatcher
+
+package mozilla.components.support.test.rule
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestResult
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.internal.TestMainDispatcher
+import kotlinx.coroutines.test.runTest
+import kotlin.reflect.full.companionObject
+import kotlin.reflect.full.companionObjectInstance
+import kotlin.reflect.full.memberProperties
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+
+/**
+ * `coroutines.test` default timeout to use when waiting for asynchronous completions of the coroutines
+ * managed by a [TestCoroutineScheduler].
+ */
+private val DEFAULT_DISPATCH_TIMEOUT_SECONDS = 60.seconds
+
+/**
+ * Convenience method of executing [testBody] in a new coroutine running with the
+ * [TestDispatcher] previously set through [Dispatchers.setMain].
+ *
+ * Running a test with a shared [TestDispatcher] allows
+ * - newly created coroutines inside it will automatically be reparented to the test coroutine context.
+ * - leveraging an already set strategy for entering launch / async blocks.
+ * - easier scheduling control.
+ *
+ * @see runTest
+ * @see Dispatchers.setMain
+ * @see TestDispatcher
+ */
+fun runTestOnMain(
+ testTimeoutMs: Duration = DEFAULT_DISPATCH_TIMEOUT_SECONDS,
+ testBody: suspend TestScope.() -> Unit,
+): TestResult {
+ val mainDispatcher = Dispatchers.Main
+ require(mainDispatcher is TestMainDispatcher) {
+ "A TestDispatcher is not available. Use MainCoroutineRule or Dispatchers.setMain to set one before calling this method"
+ }
+
+ // Get the TestDispatcher set through `Dispatchers.setMain(..)`.
+ val companionObject = mainDispatcher::class.companionObject
+ val companionInstance = mainDispatcher::class.companionObjectInstance
+ val testDispatcher = companionObject!!.memberProperties.first().getter.call(companionInstance) as TestDispatcher
+
+ // Delegate to the original implementation of `runTest`. Just with a previously set TestDispatcher.
+ runTest(testDispatcher, timeout = testTimeoutMs, testBody)
+}
diff --git a/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/MainCoroutineRule.kt b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/MainCoroutineRule.kt
new file mode 100644
index 0000000000..b8df5e7ef6
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/main/java/mozilla/components/support/test/rule/MainCoroutineRule.kt
@@ -0,0 +1,56 @@
+/* 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.test.rule
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import mozilla.components.support.base.utils.NamedThreadFactory
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import java.util.concurrent.Executors
+
+/**
+ * Create single threaded dispatcher for test environment.
+ */
+@Deprecated("Use a `TestDispatcher` from the kotlinx-coroutines-test library", ReplaceWith("UnconfinedTestDispatcher()"))
+fun createTestCoroutinesDispatcher(): CoroutineDispatcher = Executors.newSingleThreadExecutor(
+ NamedThreadFactory("TestCoroutinesDispatcher"),
+).asCoroutineDispatcher()
+
+/**
+ * JUnit rule to change Dispatchers.Main in coroutines.
+ * This assumes no other calls to `Dispatchers.setMain` are made to override the main dispatcher.
+ *
+ * @param testDispatcher [TestDispatcher] for handling all coroutines execution.
+ * Defaults to [UnconfinedTestDispatcher] which will eagerly enter `launch` or `async` blocks.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class MainCoroutineRule(val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()) : TestWatcher() {
+ /**
+ * Get a [TestScope] that integrates with `runTest` and can be passed as an argument
+ * to the code under test when a [CoroutineScope] is required.
+ *
+ * This will rely on [testDispatcher] for controlling entering `launch` or `async` blocks.
+ */
+ val scope by lazy { TestScope(testDispatcher) }
+
+ override fun starting(description: Description) {
+ Dispatchers.setMain(testDispatcher)
+ super.starting(description)
+ }
+
+ override fun finished(description: Description) {
+ Dispatchers.resetMain()
+ super.finished(description)
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/test/java/PermissionsTest.kt b/mobile/android/android-components/components/support/test/src/test/java/PermissionsTest.kt
new file mode 100644
index 0000000000..1182821787
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/test/java/PermissionsTest.kt
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.test.robolectric
+
+import android.Manifest.permission.INTERNET
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.ktx.android.content.isPermissionGranted
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class PermissionsTest {
+
+ @Test
+ fun `after call grantPermission this permission must be granted `() {
+ var isGranted = testContext.isPermissionGranted(INTERNET)
+ assertFalse(isGranted)
+
+ grantPermission(INTERNET)
+ isGranted = testContext.isPermissionGranted(INTERNET)
+
+ assertTrue(isGranted)
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/ThrowPropertyTest.kt b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/ThrowPropertyTest.kt
new file mode 100644
index 0000000000..24e50c2b6d
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/ThrowPropertyTest.kt
@@ -0,0 +1,24 @@
+/* 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.test.robolectric.mozilla.components.support.test
+
+import mozilla.components.support.test.ThrowProperty
+import org.junit.Test
+
+class ThrowPropertyTest {
+ private val testProperty = "test"
+
+ @Test(expected = UnsupportedOperationException::class)
+ fun `exception thrown when get value is called`() {
+ val throwProperty = ThrowProperty<String>()
+ throwProperty.getValue(testProperty, ::testProperty)
+ }
+
+ @Test(expected = UnsupportedOperationException::class)
+ fun `exception thrown when set value is called`() {
+ val throwProperty = ThrowProperty<String>()
+ throwProperty.setValue(testProperty, ::testProperty, "test1")
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/file/ResourcesTest.kt b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/file/ResourcesTest.kt
new file mode 100644
index 0000000000..58cd279fcd
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/file/ResourcesTest.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.support.test.robolectric.mozilla.components.support.test.file
+
+import mozilla.components.support.test.file.loadResourceAsString
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ResourcesTest {
+
+ @Test
+ fun getProvidedAppContext() {
+ assertEquals("42", loadResourceAsString("/example_file.txt"))
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/ExtensionsTest.kt b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/ExtensionsTest.kt
new file mode 100644
index 0000000000..6b86a649a5
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/ExtensionsTest.kt
@@ -0,0 +1,23 @@
+/* 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.test.robolectric
+
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ExtensionsTest {
+
+ @Test
+ fun getProvidedAppContext() {
+ Assert.assertEquals(
+ ApplicationProvider.getApplicationContext(),
+ testContext,
+ )
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/FragmentsTest.kt b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/FragmentsTest.kt
new file mode 100644
index 0000000000..eca68aa7e7
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/test/java/mozilla/components/support/test/robolectric/FragmentsTest.kt
@@ -0,0 +1,30 @@
+/* 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.test.robolectric
+
+import androidx.fragment.app.Fragment
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class FragmentsTest {
+
+ @Test
+ fun `setupFragment should add fragment correctly`() {
+ val addedFragment = createAddedTestFragment { Fragment() }
+
+ assertTrue(addedFragment.isAdded)
+ }
+
+ @Test
+ fun `setupFragment should add fragment with correct tag`() {
+ val fragment = createAddedTestFragment(fragmentTag = "aTag") { Fragment() }
+
+ assertNotNull(fragment.parentFragmentManager.findFragmentByTag("aTag"))
+ }
+}
diff --git a/mobile/android/android-components/components/support/test/src/test/resources/example_file.txt b/mobile/android/android-components/components/support/test/src/test/resources/example_file.txt
new file mode 100644
index 0000000000..f70d7bba4a
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/test/resources/example_file.txt
@@ -0,0 +1 @@
+42 \ No newline at end of file
diff --git a/mobile/android/android-components/components/support/test/src/test/resources/robolectric.properties b/mobile/android/android-components/components/support/test/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..932b01b9eb
--- /dev/null
+++ b/mobile/android/android-components/components/support/test/src/test/resources/robolectric.properties
@@ -0,0 +1 @@
+sdk=28