Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion ChatApp/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ dependencies {

// App functions
implementation(libs.androidx.appfunctions)
implementation(libs.androidx.appfunctions.service)
ksp(libs.androidx.appfunctions.compiler)

testImplementation(libs.junit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class AppFunctionsViewModelTest {
fun toggleFunction_doesNotCrash() =
runTest {
// Use a potentially real ID from the app

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: seems this is fixed?

viewModel.toggleFunction("com.example.chatapp.appfunctions.AppFunctions#searchContacts", true)
viewModel.toggleFunction(com.example.chatapp.appfunctions.AppFunctionsIds.SEARCH_CONTACTS_ID, true)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ class AppFunctionInstrumentationTest {

@Inject lateinit var recipientsRepository: RecipientsRepository

@Inject lateinit var appFunctions: AppFunctions

private val context: Context = ApplicationProvider.getApplicationContext()
private val appFunctionManager: AppFunctionManager =
checkNotNull(AppFunctionManager.getInstance(context))
Expand Down Expand Up @@ -164,10 +162,10 @@ class AppFunctionInstrumentationTest {
.getAppFunctionDataList(
ExecuteAppFunctionResponse.Success.PROPERTY_RETURN_VALUE,
)
?.map { it.deserialize(AppFunctions.ContactSearchResult::class.java) },
?.map { it.deserialize(ContactSearchResult::class.java) },
)
.containsExactly(
AppFunctions.ContactSearchResult(endpointValue = "1", endpointType = "INDIVIDUAL", displayName = "Alice Smith"),
ContactSearchResult(endpointValue = "1", endpointType = "INDIVIDUAL", displayName = "Alice Smith"),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package com.example.chatapp.appfunctions
import android.content.Context
import android.net.Uri
import androidx.appfunctions.AppFunctionAppUnknownException
import androidx.appfunctions.AppFunctionContext
import androidx.appfunctions.AppFunctionElementNotFoundException
import androidx.appfunctions.AppFunctionInvalidArgumentException
import androidx.test.core.app.ApplicationProvider
Expand All @@ -38,12 +37,6 @@ import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class AppFunctionsTest {
private val testContext =
object : AppFunctionContext {
override val context: Context
get() = ApplicationProvider.getApplicationContext()
}

private class MockMessageRepository : MessageRepository {
var shouldFail = false
private val messages = MutableStateFlow<Map<String, List<DisplayMessage>>>(emptyMap())
Expand Down Expand Up @@ -85,19 +78,47 @@ class AppFunctionsTest {

private val callManager = CallManager(recipientsRepository)

private val appFunctions = AppFunctions(messageRepository, recipientsRepository, callManager)
private class TestChatAppFunctionService(
val testContext: Context,
messageRepo: MessageRepository,
recipientsRepo: RecipientsRepository,
callMgr: CallManager,
) : BaseChatAppFunctionService() {
init {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll suggest use AppFunctionManager API to test so that the service doesn't need to be faked. That will be our testing suggestion for app developer as well.

In general, the testing is done as following

  1. Use UiAutomation to adopt shell permission to obtain EXECUTE_APP_FUNCTION permission
  2. Use Jetpack AppFunctionManager to search AppFunctions from your target app package name. Then developers can assert the functions are exposed correctly.
  3. Use Jetpack AppFunctionManager to execute it.

This will ensure that the testing is e2e and represent how agent will use it. The mocking test has a problem that if developer setup the manifest or something incorrectly, it would not catch it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attachBaseContext(testContext)
this.messageRepository = messageRepo
this.recipientsRepository = recipientsRepo
this.callManager = callMgr
}

override fun onExecuteFunction(
request: androidx.appfunctions.ExecuteAppFunctionRequest,
cancellationSignal: android.os.CancellationSignal,
callback: java.util.function.Consumer<androidx.appfunctions.ExecuteAppFunctionResponse>,
) {
// Not used in direct unit tests
}
}

private val appFunctions =
TestChatAppFunctionService(
ApplicationProvider.getApplicationContext(),
messageRepository,
recipientsRepository,
callManager,
)

@Test(expected = AppFunctionInvalidArgumentException::class)
fun searchContacts_returnsEmptyList() {
runBlocking {
appFunctions.searchContacts(testContext, "nonexistent", "INDIVIDUAL")
appFunctions.searchContacts("nonexistent", "INDIVIDUAL")
}
}

@Test
fun searchContacts_returnsMatches() {
runBlocking {
val contacts = appFunctions.searchContacts(testContext, "Alice", "INDIVIDUAL")
val contacts = appFunctions.searchContacts("Alice", "INDIVIDUAL")
Assert.assertEquals(1, contacts.size)
Assert.assertEquals("Alice Smith", contacts[0].displayName)
}
Expand All @@ -106,7 +127,7 @@ class AppFunctionsTest {
@Test
fun searchContacts_groups_returnsMatches() {
runBlocking {
val contacts = appFunctions.searchContacts(testContext, "Work", "GROUP")
val contacts = appFunctions.searchContacts("Work", "GROUP")
Assert.assertEquals(1, contacts.size)
Assert.assertEquals("Work Friends", contacts[0].displayName)
}
Expand All @@ -115,16 +136,15 @@ class AppFunctionsTest {
@Test
fun searchContacts_emptyQuery_returnsRecent() {
runBlocking {
val contacts = appFunctions.searchContacts(testContext, "", "INDIVIDUAL")
val contacts = appFunctions.searchContacts("", "INDIVIDUAL")
Assert.assertEquals(3, contacts.size)
}
}

@Test
fun searchContacts_anyType_returnsMatches() {
runBlocking {
// "searchAny" branch (when filterType is not INDIVIDUAL or GROUP)
val contacts = appFunctions.searchContacts(testContext, "Alice", "ANY")
val contacts = appFunctions.searchContacts("Alice", "ANY")
Assert.assertEquals(1, contacts.size)
Assert.assertEquals("Alice Smith", contacts[0].displayName)
}
Expand All @@ -133,9 +153,9 @@ class AppFunctionsTest {
@Test
fun send_validMessage_returnsSuccess() {
runTest {
val result = appFunctions.send(testContext, "1", "Hello")
val result = appFunctions.send("1", "Hello")
Assert.assertEquals(
AppFunctions.Result(
Result(
"Message ID",
"Message sent to: Alice Smith.",
),
Expand All @@ -149,13 +169,12 @@ class AppFunctionsTest {
runTest {
val result =
appFunctions.send(
testContext,
"1",
"Hello",
listOf(Uri.parse("content://media/1")),
)
Assert.assertEquals(
AppFunctions.Result(
Result(
"Message ID",
"Message sent to: Alice Smith.",
),
Expand All @@ -167,9 +186,9 @@ class AppFunctionsTest {
@Test
fun send_toGroup_success() {
runTest {
val result = appFunctions.send(testContext, "g1", "Hello")
val result = appFunctions.send("g1", "Hello")
Assert.assertEquals(
AppFunctions.Result(
Result(
"Message ID",
"Message sent to: Work Friends.",
),
Expand All @@ -181,39 +200,37 @@ class AppFunctionsTest {
@Test(expected = AppFunctionInvalidArgumentException::class)
fun send_emptyContent_fails() {
runTest {
appFunctions.send(testContext, "1", "")
appFunctions.send("1", "")
}
}

@Test(expected = AppFunctionElementNotFoundException::class)
fun send_invalidRecipient_fails() {
runTest {
appFunctions.send(testContext, "nonexistent_id", "Hello")
appFunctions.send("nonexistent_id", "Hello")
}
}

@Test(expected = AppFunctionAppUnknownException::class)
fun send_repositoryError_returnsError() {
runTest {
messageRepository.shouldFail = true
appFunctions.send(testContext, "1", "Hello")
appFunctions.send("1", "Hello")
}
}

@Test
fun makeCall_returnsPendingIntent() {
runBlocking {
val pendingIntent = appFunctions.makeCall(testContext, endpointValue = "1")
val pendingIntent = appFunctions.makeCall(endpointValue = "1")
Assert.assertNotNull(pendingIntent)
}
}


@Test(expected = AppFunctionElementNotFoundException::class)
fun makeCall_invalidEndpointValue_fails() {
runBlocking {
appFunctions.makeCall(testContext, endpointValue = "nonexistent_id")
appFunctions.makeCall(endpointValue = "nonexistent_id")
}
}

}
18 changes: 17 additions & 1 deletion ChatApp/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions">
xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"
xmlns:tools="http://schemas.android.com/tools">

<application
android:name=".ChatApplication"
Expand Down Expand Up @@ -49,6 +50,21 @@
android:host="com.example.chatapp" />
</intent-filter>
</activity>
<service
android:name="com.example.chatapp.appfunctions.ChatAppFunctionService"
android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
android:exported="true"
tools:targetApi="36">
<property
android:name="android.app.appfunctions.schema"
android:value="app_functions_schema.xsd" />
<property
android:name="android.app.appfunctions.v2"
android:value="chat_app_function_service.xml" />
<intent-filter>
<action android:name="android.app.appfunctions.AppFunctionService" />
</intent-filter>
</service>
<property
android:name="android.app.appfunctions.app_metadata"
android:resource="@xml/app_metadata" />
Expand Down

This file was deleted.

Loading