UnitTestFilters.kt
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.glance.appwidget.testing.unit
import android.app.Service
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Intent
import android.os.Bundle
import androidx.glance.EmittableCheckable
import androidx.glance.action.ActionModifier
import androidx.glance.action.ActionParameters
import androidx.glance.action.actionParametersOf
import androidx.glance.appwidget.EmittableCircularProgressIndicator
import androidx.glance.appwidget.EmittableLinearProgressIndicator
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.RunCallbackAction
import androidx.glance.appwidget.action.SendBroadcastActionAction
import androidx.glance.appwidget.action.SendBroadcastClassAction
import androidx.glance.appwidget.action.SendBroadcastComponentAction
import androidx.glance.appwidget.action.SendBroadcastIntentAction
import androidx.glance.appwidget.action.StartActivityIntentAction
import androidx.glance.appwidget.action.StartServiceClassAction
import androidx.glance.appwidget.action.StartServiceComponentAction
import androidx.glance.appwidget.action.StartServiceIntentAction
import androidx.glance.testing.GlanceNodeAssertionsProvider
import androidx.glance.testing.GlanceNodeMatcher
import androidx.glance.testing.unit.MappedNode
/**
* Returns a matcher that matches if a node is checkable (e.g. radio button, switch, checkbox)
* and is checked.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*/
fun isChecked(): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
description = "is checked"
) { node ->
val emittable = node.value.emittable
emittable is EmittableCheckable && emittable.checked
}
/**
* Returns a matcher that matches if a node is checkable (e.g. radio button, switch, checkbox)
* but is not checked.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*/
fun isNotChecked(): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
description = "is not checked"
) { node ->
val emittable = node.value.emittable
emittable is EmittableCheckable && !emittable.checked
}
/**
* Returns a matcher that matches if a node has a clickable set with action that runs a callback.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*
* @param callbackClass an implementation of [ActionCallback] that is expected to have been passed
* in the `actionRunCallback` method call
* @param parameters the parameters associated with the action that are expected to have been passed
* in the `actionRunCallback` method call
*/
@PublishedApi // See b/316353540; a reified version of this is available in the public api.
internal fun <T : ActionCallback> hasRunCallbackClickAction(
callbackClass: Class<T>,
parameters: ActionParameters = actionParametersOf()
): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
"has run callback click action with callback class: ${callbackClass.name} and " +
"parameters: $parameters"
) { node ->
node.value.emittable.modifier.any {
if (it is ActionModifier) {
val action = it.action
if (action is RunCallbackAction) {
return@any action.callbackClass == callbackClass && action.parameters == parameters
}
}
false
}
}
/**
* Returns a matcher that matches if a node has a clickable set with action that runs a callback.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*
* @param T callback class that is expected to have been passed in the `actionRunCallback` method
* call
* @param parameters the parameters associated with the action that are expected to have been passed
* in the `actionRunCallback` method call
*/
inline fun <reified T : ActionCallback> hasRunCallbackClickAction(
parameters: ActionParameters = actionParametersOf()
): GlanceNodeMatcher<MappedNode> = hasRunCallbackClickAction(
callbackClass = T::class.java,
parameters = parameters
)
/**
* Returns a matcher that matches if a node has a clickable set with action that starts an activity.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*
* @param intent the intent for launching an activity that is expected to have been passed in the
* `actionStartActivity` method call. Note: Only matches if intents are same per
* `filterEquals`.
* @param parameters the parameters associated with the action that are expected to have been passed
* in the `actionStartActivity` method call
* @param activityOptions Additional options built from an [android.app.ActivityOptions] to apply to
* an activity start.
*/
// Other variants in the base layer (glance-testing).
fun hasStartActivityClickAction(
intent: Intent,
parameters: ActionParameters = actionParametersOf(),
activityOptions: Bundle? = null
): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
if (activityOptions != null) {
"has start activity click action with intent: $intent, " +
"parameters: $parameters and bundle: $activityOptions"
} else {
"has start activity click action with intent: $intent and parameters: $parameters"
}
) { node ->
node.value.emittable.modifier.any {
if (it is ActionModifier) {
val action = it.action
if (action is StartActivityIntentAction) {
var result = action.intent.filterEquals(intent) &&
action.parameters == parameters
if (activityOptions != null) {
result = result && activityOptions == action.activityOptions
}
return@any result
}
}
false
}
}
/**
* Returns a matcher that matches if a node has a clickable set with action that starts a service.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*
* @param serviceClass class of the service to launch that is expected to have been passed in the
* `actionStartService` method call.
* @param isForegroundService if the service to launch is expected to have been set as foreground
* service in the `actionStartService` method call.
*/
@PublishedApi // See b/316353540; a reified version of this is available in the public api.
internal fun hasStartServiceAction(
serviceClass: Class<out Service>,
isForegroundService: Boolean = false
): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
description = if (isForegroundService) {
"has start service action for foreground service: ${serviceClass.name}"
} else {
"has start service action for non-foreground service: ${serviceClass.name}"
}
) { node ->
node.value.emittable.modifier.any {
if (it is ActionModifier) {
val action = it.action
if (action is StartServiceClassAction) {
return@any action.serviceClass == serviceClass &&
action.isForegroundService == isForegroundService
}
}
false
}
}
/**
* Returns a matcher that matches if a node has a clickable set with action that starts a service.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*
* @param T class of the service to launch that is expected to have been passed in the
* `actionStartService` method call.
* @param isForegroundService if the service to launch is expected to have been set as foreground
* service in the `actionStartService` method call.
*/
inline fun <reified T : Service> hasStartServiceAction(
isForegroundService: Boolean = false
): GlanceNodeMatcher<MappedNode> = hasStartServiceAction(
serviceClass = T::class.java,
isForegroundService = isForegroundService
)
/**
* Returns a matcher that matches if a node has a clickable set with action that starts a service.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*
* @param componentName component of the service to launch that is expected to have been passed in
* the `actionStartService` method call.
* @param isForegroundService if the service to launch is expected to have been set as foreground
* service in the `actionStartService` method call.
*/
internal fun hasStartServiceAction(
componentName: ComponentName,
isForegroundService: Boolean = false
): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
description = if (isForegroundService) {
"has start service action for foreground service component: $componentName"
} else {
"has start service action for non-foreground service component: $componentName"
}
) { node ->
node.value.emittable.modifier.any {
if (it is ActionModifier) {
val action = it.action
if (action is StartServiceComponentAction) {
return@any action.componentName == componentName &&
action.isForegroundService == isForegroundService
}
}
false
}
}
/**
* Returns a matcher that matches if a node has a clickable set with action that starts a service.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*
* @param intent the intent for launching the service that is expected to have been passed in
* the `actionStartService` method call.
* @param isForegroundService if the service to launch is expected to have been set as foreground
* service in the `actionStartService` method call.
*/
fun hasStartServiceAction(
intent: Intent,
isForegroundService: Boolean = false
): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
description = if (isForegroundService) {
"has start service action for foreground service: $intent"
} else {
"has start service action for non-foreground service: $intent"
}
) { node ->
node.value.emittable.modifier.any {
if (it is ActionModifier) {
val action = it.action
if (action is StartServiceIntentAction) {
return@any action.intent == intent &&
action.isForegroundService == isForegroundService
}
}
false
}
}
/**
* Returns a matcher that matches if a node has a clickable set with action that sends a broadcast.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*
* @param receiverClass class of the broadcast receiver that is expected to have been passed in the
* actionSendBroadcast` method call.
*/
@PublishedApi // See b/316353540; a reified version of this is available in the public api.
internal fun hasSendBroadcastAction(
receiverClass: Class<out BroadcastReceiver>
): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
description = "has send broadcast action for receiver class: ${receiverClass.name}"
) { node ->
node.value.emittable.modifier.any {
if (it is ActionModifier) {
val action = it.action
if (action is SendBroadcastClassAction) {
return@any action.receiverClass == receiverClass
}
}
false
}
}
/**
* Returns a matcher that matches if a node has a clickable set with action that sends a broadcast.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*
* @param T class of the broadcast receiver that is expected to have been passed in the
* actionSendBroadcast` method call.
*/
inline fun <reified T : BroadcastReceiver> hasSendBroadcastAction(): GlanceNodeMatcher<MappedNode> =
hasSendBroadcastAction(T::class.java)
/**
* Returns a matcher that matches if a node has a clickable set with action that sends a broadcast.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*
* @param intentAction the intent action of the broadcast receiver that is expected to have been
* passed in the `actionSendBroadcast` method call.
* @param componentName optional [ComponentName] of the target broadcast receiver that is expected
* to have been passed in the actionSendBroadcast` method call.
*/
fun hasSendBroadcastAction(
intentAction: String,
componentName: ComponentName? = null
): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
description =
if (componentName != null) {
"has send broadcast action with intent action: $intentAction and component: $componentName"
} else {
"has send broadcast action with intent action: $intentAction"
}
) { node ->
node.value.emittable.modifier.any {
if (it is ActionModifier) {
val action = it.action
if (action is SendBroadcastActionAction) {
var result = action.action == intentAction
if (componentName != null) {
result = result && action.componentName == componentName
}
return@any result
}
}
false
}
}
/**
* Returns a matcher that matches if a node has a clickable set with action that sends a broadcast.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*
* @param componentName [ComponentName] of the target broadcast receiver that is expected to have
* been passed in the actionSendBroadcast` method call.
*/
fun hasSendBroadcastAction(
componentName: ComponentName
): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
description = "has send broadcast action with component: $componentName"
) { node ->
node.value.emittable.modifier.any {
if (it is ActionModifier) {
val action = it.action
if (action is SendBroadcastComponentAction) {
return@any action.componentName == componentName
}
}
false
}
}
/**
* Returns a matcher that matches if a node has a clickable set with action that sends a broadcast.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*
* @param intent the intent for sending broadcast that is expected to have been passed in the
* `actionSendBroadcast` method call. Note: intent is only matched using filterEquals.
*/
fun hasSendBroadcastAction(
intent: Intent
): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
description = "has send broadcast action with intent: $intent"
) { node ->
node.value.emittable.modifier.any {
if (it is ActionModifier) {
val action = it.action
if (action is SendBroadcastIntentAction) {
return@any action.intent.filterEquals(intent)
}
}
false
}
}
/**
* Returns a matcher that matches if a given node is a linear progress indicator with given progress
* value.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*
* @param progress the expected value of the current progress
*/
fun isLinearProgressIndicator(
/*@FloatRange(from = 0.0, to = 1.0)*/
progress: Float
): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
description = "is a linear progress indicator with progress value: $progress"
) { node ->
val emittable = node.value.emittable
emittable is EmittableLinearProgressIndicator &&
!emittable.indeterminate &&
emittable.progress == progress
}
/**
* Returns a matcher that matches if a given node is an indeterminate progress bar.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*/
fun isIndeterminateLinearProgressIndicator(): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
description = "is an indeterminate linear progress indicator"
) { node ->
val emittable = node.value.emittable
emittable is EmittableLinearProgressIndicator && emittable.indeterminate
}
/**
* Returns a matcher that matches if a given node is an indeterminate circular progress indicator.
*
* This can be passed in [GlanceNodeAssertionsProvider.onNode] and
* [GlanceNodeAssertionsProvider.onAllNodes] functions on assertion providers to filter out
* matching node(s) or in assertions to validate that node(s) satisfy the condition.
*/
fun isIndeterminateCircularProgressIndicator(): GlanceNodeMatcher<MappedNode> = GlanceNodeMatcher(
description = "is an indeterminate circular progress indicator"
) { node ->
node.value.emittable is EmittableCircularProgressIndicator
}