From b32b82c690b91634c0363448c2db122991ec1745 Mon Sep 17 00:00:00 2001 From: OrangeAndGreen Date: Mon, 2 Nov 2026 16:07:49 -0500 Subject: [PATCH 1/2] [AI] Screenshot the Opportunity Home surfaces for localization review Adds opt-in l10n screenshots of the job intro, learn modules sheet, learning progress and the four delivery tabs. The mock Connect server now serves learn and delivery progress through the real parsers, and a screen-wide capture picks up the bottom sheet's own window. Co-Authored-By: Claude Opus 5.5 --- .../l10n/BaseConnectScreenshotTest.kt | 14 +- .../commcare/l10n/ConnectScreenshotData.kt | 152 ++++++++++++++++-- .../src/org/commcare/l10n/L10nScreenshots.kt | 23 ++- .../l10n/OpportunityHomeScreenshotTest.kt | 143 ++++++++++++++++ 4 files changed, 315 insertions(+), 17 deletions(-) create mode 100644 app/unit-tests/src/org/commcare/l10n/OpportunityHomeScreenshotTest.kt diff --git a/app/unit-tests/src/org/commcare/l10n/BaseConnectScreenshotTest.kt b/app/unit-tests/src/org/commcare/l10n/BaseConnectScreenshotTest.kt index a4bfb746f1..1506008cad 100644 --- a/app/unit-tests/src/org/commcare/l10n/BaseConnectScreenshotTest.kt +++ b/app/unit-tests/src/org/commcare/l10n/BaseConnectScreenshotTest.kt @@ -29,7 +29,7 @@ import org.junit.rules.TestRule /** * The showcase user signed in to Connect, whose screens read seeded data from the local database, with every - * network call answered empty so none of it is pruned. + * network call answered by [responseBody], empty unless a test says otherwise, so none of it is pruned. */ abstract class BaseConnectScreenshotTest( protected val target: ScreenshotTarget, @@ -37,12 +37,12 @@ abstract class BaseConnectScreenshotTest( @get:Rule val localeRule: TestRule = L10nScreenshots.localeRule(target) - private val mockApi = ConnectMockApiServer() + protected val mockApi = ConnectMockApiServer() @Before fun setUpConnect() { mockApi.start() - mockApi.server.dispatcher = emptyJsonDispatcher() + mockApi.server.dispatcher = showcaseDispatcher() ConnectRepository.resetInstance() ConnectRequestManager.cancelAll() mockOutsideWorld() @@ -86,8 +86,12 @@ abstract class BaseConnectScreenshotTest( every { AppUtils.isAppInstalled(any()) } returns true } - private fun emptyJsonDispatcher(): Dispatcher = + /** The JSON body the mock Connect server answers a request for [path] with. */ + protected open fun responseBody(path: String): String = "{}" + + private fun showcaseDispatcher(): Dispatcher = object : Dispatcher() { - override fun dispatch(request: RecordedRequest): MockResponse = MockResponse().setResponseCode(200).setBody("{}") + override fun dispatch(request: RecordedRequest): MockResponse = + MockResponse().setResponseCode(200).setBody(responseBody(request.requestUrl?.encodedPath.orEmpty())) } } diff --git a/app/unit-tests/src/org/commcare/l10n/ConnectScreenshotData.kt b/app/unit-tests/src/org/commcare/l10n/ConnectScreenshotData.kt index 33743b4393..3725744a67 100644 --- a/app/unit-tests/src/org/commcare/l10n/ConnectScreenshotData.kt +++ b/app/unit-tests/src/org/commcare/l10n/ConnectScreenshotData.kt @@ -20,8 +20,17 @@ object ConnectScreenshotData { private const val ORGANIZATION = "Kisumu Community Health Partners" private const val CURRENCY = "INR" private const val LEARN_MODULES = 5 + private const val LEARNING_MODULES_DONE = 3 private const val MAX_VISITS = 40 + private const val BUDGET_PER_VISIT = 1250 + private val HOUSEHOLDS = listOf("Achieng Odhiambo", "Baraka Mwangi", "Wanjiku Kamau", "Otieno Family", "Nyambura Njoroge") + private const val DELIVERY_VISITS = 27 + private const val VISITS_PER_DAY = 4 + private const val PAYMENT_UNIT_ID = "household-visit" const val CONVERSATION_CHANNEL_ID = "showcase-channel-partners" + const val LEARNING_OPPORTUNITY_ID = 1 + const val DELIVERING_OPPORTUNITY_ID = 2 + const val NEW_OPPORTUNITY_ID = 4 /** A PersonalID user with a full name, a Kenyan phone number and an email. */ fun user(hasConnectAccess: Boolean = false): ConnectUserRecord = @@ -43,20 +52,107 @@ object ConnectScreenshotData { /** One opportunity per state the opportunity list shows, with a long title and an expiry warning among them. */ fun opportunities(): List = listOf( - opportunity(1, "Household Malaria Prevention Survey", ConnectJobRecord.STATUS_LEARNING, endInDays = 21, modulesDone = 3), opportunity( - 2, + LEARNING_OPPORTUNITY_ID, + "Household Malaria Prevention Survey", + ConnectJobRecord.STATUS_LEARNING, + endInDays = 21, + modulesDone = LEARNING_MODULES_DONE, + ), + opportunity( + DELIVERING_OPPORTUNITY_ID, "Child Growth Monitoring and Nutrition Counselling, Kisumu County", ConnectJobRecord.STATUS_DELIVERING, endInDays = 2, - visitsDone = 27, + visitsDone = DELIVERY_VISITS, ), opportunity(3, "Maternal Health Home Visits", ConnectJobRecord.STATUS_DELIVERING, endInDays = 60, visitsDone = 12), - opportunity(4, "Community Vaccination Awareness Campaign", ConnectJobRecord.STATUS_AVAILABLE_NEW, endInDays = 45), + opportunity( + NEW_OPPORTUNITY_ID, + "Community Vaccination Awareness Campaign", + ConnectJobRecord.STATUS_AVAILABLE_NEW, + endInDays = 45, + ), opportunity(5, "Polio Campaign Follow-up", ConnectJobRecord.STATUS_DELIVERING, endInDays = -14, visitsDone = MAX_VISITS), opportunity(6, "Bed Net Distribution Check", ConnectJobRecord.STATUS_DELIVERING, endInDays = -40, visitsDone = 18), ) + /** The opportunity UUID the server and the local database key the opportunity with [id] by. */ + fun opportunityUuid(id: Int): String = "showcase-opportunity-$id" + + /** + * The learn progress response for the opportunity with [id]: the learning opportunity is part way through its + * modules, and every other one has finished them and passed the assessment. + */ + fun learningProgressResponse(id: Int): String { + val modulesDone = if (id == LEARNING_OPPORTUNITY_ID) LEARNING_MODULES_DONE else LEARN_MODULES + return JSONObject() + .put( + "completed_modules", + JSONArray().apply { + (1..modulesDone).forEach { module -> + put( + JSONObject().apply { + put("id", module) + put("date", isoDateTime(minutesAgo = (40 - module * 6) * 24 * 60)) + put("module", module) + put("duration", "01:45:00") + }, + ) + } + }, + ).put( + "assessments", + JSONArray().apply { + if (modulesDone == LEARN_MODULES) { + put( + JSONObject().apply { + put("date", isoDateTime(minutesAgo = 9 * 24 * 60)) + put("score", 92) + put("passing_score", 80) + put("passed", true) + }, + ) + } + }, + ).toString() + } + + /** + * The delivery progress response for the delivering opportunity: its visits spread over the last week, the newest + * of them still pending and one rejected, and two confirmed payments ahead of one still to confirm. + */ + fun deliveryProgressResponse(): String { + val rejectedVisit = 7 + val pendingVisits = 2 + val approvedVisits = DELIVERY_VISITS - pendingVisits - 1 + return JSONObject() + .put("max_payments", MAX_VISITS) + .put("payment_accrued", approvedVisits * BUDGET_PER_VISIT) + .put("is_user_suspended", false) + .put( + "deliveries", + JSONArray().apply { + (0 until DELIVERY_VISITS).forEach { visit -> + val status = + when { + visit < pendingVisits -> "pending" + visit == rejectedVisit -> "rejected" + else -> "approved" + } + put(delivery(visit, status, minutesAgo = (visit / VISITS_PER_DAY) * 24 * 60 + 30 + visit * 20)) + } + }, + ).put( + "payments", + JSONArray().apply { + put(payment("1", visits = 12, daysAgo = 14, confirmed = true)) + put(payment("2", visits = 6, daysAgo = 7, confirmed = true)) + put(payment("3", visits = 5, daysAgo = 1, confirmed = false)) + }, + ).toString() + } + /** Subscribed channels with unread, read and sent last messages, plus one the worker has unsubscribed from. */ fun channels(): List = listOf( @@ -199,7 +295,7 @@ object ConnectScreenshotData { ): JSONObject = JSONObject().apply { put("id", id) - put("opportunity_id", "showcase-opportunity-$id") + put("opportunity_id", opportunityUuid(id)) put("name", title) put("description", "Visit households in your area, record what you find and share health guidance with each family.") put("short_description", "Visit households and record what you find.") @@ -208,8 +304,8 @@ object ConnectScreenshotData { put("end_date", isoDate(endInDays)) put("max_visits_per_user", MAX_VISITS) put("daily_max_visits_per_user", 8) - put("budget_per_visit", 1250) - put("budget_per_user", 1250 * MAX_VISITS) + put("budget_per_visit", BUDGET_PER_VISIT) + put("budget_per_user", BUDGET_PER_VISIT * MAX_VISITS) put("currency", CURRENCY) put("deliver_progress", visitsDone) put("payment_units", paymentUnitsJson()) @@ -228,14 +324,48 @@ object ConnectScreenshotData { JSONArray().put( JSONObject().apply { put("id", 1) - put("payment_unit_id", "household-visit") + put("payment_unit_id", PAYMENT_UNIT_ID) put("name", "Household visit") put("max_total", MAX_VISITS) put("max_daily", 8) - put("amount", 1250) + put("amount", BUDGET_PER_VISIT) }, ) + private fun delivery( + visit: Int, + status: String, + minutesAgo: Int, + ): JSONObject = + JSONObject().apply { + put("id", visit + 1) + put("visit_date", isoDateTime(minutesAgo)) + put("status", status) + put("deliver_unit_name", "Household visit") + put("deliver_unit_slug", PAYMENT_UNIT_ID) + put("deliver_unit_slug_id", PAYMENT_UNIT_ID) + put("entity_id", "showcase-household-$visit") + put("entity_name", HOUSEHOLDS[visit % HOUSEHOLDS.size]) + put("reason", if (status == "rejected") "The photo of the growth chart is missing." else "") + } + + private fun payment( + id: String, + visits: Int, + daysAgo: Int, + confirmed: Boolean, + ): JSONObject = + JSONObject().apply { + put("id", id) + put("payment_id", "showcase-payment-$id") + put("date_paid", isoDateTime(minutesAgo = daysAgo * 24 * 60)) + put("amount", visits * BUDGET_PER_VISIT) + put("confirmed", confirmed) + if (confirmed) { + put("date_confirmed", isoDate(-daysAgo + 1)) + } + } + private fun appJson( appId: String, withModules: Boolean, @@ -267,6 +397,10 @@ object ConnectScreenshotData { } } + private fun isoDateTime(minutesAgo: Int): String = + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US) + .format(Date(System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(minutesAgo.toLong()))) + private fun isoDate(daysFromNow: Int): String = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, daysFromNow) }.time) } diff --git a/app/unit-tests/src/org/commcare/l10n/L10nScreenshots.kt b/app/unit-tests/src/org/commcare/l10n/L10nScreenshots.kt index c29b9b0a8b..abba7474be 100644 --- a/app/unit-tests/src/org/commcare/l10n/L10nScreenshots.kt +++ b/app/unit-tests/src/org/commcare/l10n/L10nScreenshots.kt @@ -1,7 +1,9 @@ package org.commcare.l10n import android.app.Activity +import com.github.takahirom.roborazzi.ExperimentalRoborazziApi import com.github.takahirom.roborazzi.captureRoboImage +import com.github.takahirom.roborazzi.captureScreenRoboImage import org.junit.Assume.assumeTrue import org.junit.rules.TestRule import org.junit.runner.Description @@ -104,12 +106,27 @@ object L10nScreenshots { ) { controller.visible() ShadowLooper.idleMainLooper() - val file = File(File(outDir, target.folder), "$screen.png") - file.parentFile?.mkdirs() controller .get() .window .decorView - .captureRoboImage(file.absolutePath) + .captureRoboImage(outputFile(target, screen).absolutePath) } + + /** Records every window on screen, so a dialog or bottom sheet shows over the activity beneath it. */ + @OptIn(ExperimentalRoborazziApi::class) + fun captureScreen( + controller: ActivityController, + target: ScreenshotTarget, + screen: String, + ) { + controller.visible() + ShadowLooper.idleMainLooper() + captureScreenRoboImage(outputFile(target, screen).absolutePath) + } + + private fun outputFile( + target: ScreenshotTarget, + screen: String, + ): File = File(File(outDir, target.folder), "$screen.png").also { it.parentFile?.mkdirs() } } diff --git a/app/unit-tests/src/org/commcare/l10n/OpportunityHomeScreenshotTest.kt b/app/unit-tests/src/org/commcare/l10n/OpportunityHomeScreenshotTest.kt new file mode 100644 index 0000000000..a439289039 --- /dev/null +++ b/app/unit-tests/src/org/commcare/l10n/OpportunityHomeScreenshotTest.kt @@ -0,0 +1,143 @@ +package org.commcare.l10n + +import android.content.Intent +import androidx.navigation.fragment.NavHostFragment +import androidx.test.core.app.ApplicationProvider +import androidx.viewpager2.widget.ViewPager2 +import org.commcare.CommCareTestApplication +import org.commcare.activities.connect.ConnectActivity +import org.commcare.connect.ConnectConstants +import org.commcare.connect.ConnectOpportunityHomeFragmentDirections +import org.commcare.connect.database.ConnectJobUtils +import org.commcare.connect.opportunityHomeSurface +import org.commcare.connect.repository.ConnectRepository +import org.commcare.connect.repository.ConnectSyncPreferences +import org.commcare.dalvik.R +import org.commcare.fragments.connect.ConnectDeliveryHomeFragment +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.ParameterizedRobolectricTestRunner +import org.robolectric.Robolectric +import org.robolectric.android.controller.ActivityController +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode +import org.robolectric.shadows.ShadowLooper + +@Config(application = CommCareTestApplication::class, sdk = [34]) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@RunWith(ParameterizedRobolectricTestRunner::class) +class OpportunityHomeScreenshotTest( + target: ScreenshotTarget, +) : BaseConnectScreenshotTest(target) { + @Before + fun seedOpportunities() { + ConnectJobUtils.storeJobs(ApplicationProvider.getApplicationContext(), ConnectScreenshotData.opportunities(), true) + ConnectSyncPreferences.getInstance().apply { + markSessionStart() + storeLastSyncTime(ConnectRepository.SYNC_KEY_OPPORTUNITIES) + } + } + + override fun responseBody(path: String): String { + val opportunityId = + OPPORTUNITY_PATH + .find(path) + ?.groupValues + ?.get(1) + ?.toIntOrNull() ?: return "{}" + return when { + path.endsWith("/learn_progress") -> { + ConnectScreenshotData.learningProgressResponse(opportunityId) + } + + path.endsWith("/delivery_progress") && opportunityId == ConnectScreenshotData.DELIVERING_OPPORTUNITY_ID -> { + ConnectScreenshotData.deliveryProgressResponse() + } + + else -> { + "{}" + } + } + } + + @Test + fun jobIntro() { + val controller = openOpportunityHome(ConnectScreenshotData.NEW_OPPORTUNITY_ID) + L10nScreenshots.capture(controller, target, "21_connect_job_intro") + } + + @Test + fun learnModulesSheet() { + val controller = openOpportunityHome(ConnectScreenshotData.NEW_OPPORTUNITY_ID) + navHostOf(controller).navController.navigate( + ConnectOpportunityHomeFragmentDirections.actionConnectOpportunityHomeFragmentToConnectLearnModulesBottomSheet(), + ) + ShadowLooper.idleMainLooper() + L10nScreenshots.captureScreen(controller, target, "22_connect_learn_modules_sheet") + } + + @Test + fun learningProgress() { + val controller = openOpportunityHome(ConnectScreenshotData.LEARNING_OPPORTUNITY_ID) + mockApi.awaitRequest() + L10nScreenshots.capture(controller, target, "23_connect_learning_progress") + } + + @Test + fun deliveryDashboard() = captureDeliveryTab(ConnectDeliveryHomeFragment.TAB_DASHBOARD, "24_connect_delivery_dashboard") + + @Test + fun deliveryPayment() = captureDeliveryTab(ConnectDeliveryHomeFragment.TAB_PAYMENT, "25_connect_delivery_payment") + + @Test + fun deliveryVisits() = captureDeliveryTab(ConnectDeliveryHomeFragment.TAB_VISITS, "26_connect_delivery_visits") + + @Test + fun deliveryMore() = captureDeliveryTab(ConnectDeliveryHomeFragment.TAB_MORE, "27_connect_delivery_more") + + /** Delivery progress syncs first, and its success fetches the learn records the More tab reads. */ + private fun captureDeliveryTab( + tab: Int, + screen: String, + ) { + val controller = openOpportunityHome(ConnectScreenshotData.DELIVERING_OPPORTUNITY_ID) + mockApi.awaitRequest() + mockApi.awaitRequest() + navHostOf(controller) + .opportunityHomeSurface() + .requireView() + .findViewById(R.id.connect_delivery_home_view_pager) + .setCurrentItem(tab, false) + ShadowLooper.idleMainLooper() + L10nScreenshots.capture(controller, target, screen) + } + + private fun openOpportunityHome(opportunityId: Int): ActivityController { + val intent = + Intent(ApplicationProvider.getApplicationContext(), ConnectActivity::class.java).apply { + putExtra(ConnectConstants.GO_TO_JOB_STATUS, true) + putExtra(ConnectConstants.OPPORTUNITY_UUID, ConnectScreenshotData.opportunityUuid(opportunityId)) + } + val controller = + Robolectric + .buildActivity(ConnectActivity::class.java, intent) + .create() + .postCreate(null) + .start() + .resume() + ShadowLooper.idleMainLooper() + return controller + } + + private fun navHostOf(controller: ActivityController): NavHostFragment = + controller.get().supportFragmentManager.findFragmentById(R.id.nav_host_fragment_connect) as NavHostFragment + + companion object { + private val OPPORTUNITY_PATH = Regex("/opportunity/showcase-opportunity-(\\d+)/") + + @JvmStatic + @ParameterizedRobolectricTestRunner.Parameters(name = "{0}") + fun locales() = L10nScreenshots.parameters() + } +} From e3e4df0d942dc3dcdbb2ce68ac2e1cacdf59f0b4 Mon Sep 17 00:00:00 2001 From: OrangeAndGreen Date: Fri, 6 Nov 2026 11:35:53 -0500 Subject: [PATCH 2/2] [AI] Name screenshot data helpers with action verbs Builders that return JSON now say so, per review. Co-Authored-By: Claude Opus 5.5 --- .../l10n/BaseConnectScreenshotTest.kt | 2 +- .../l10n/ChannelListScreenshotTest.kt | 4 +- .../l10n/ChannelMessagesScreenshotTest.kt | 4 +- .../commcare/l10n/ConnectScreenshotData.kt | 120 ++++++++++-------- .../l10n/OpportunityHomeScreenshotTest.kt | 8 +- .../l10n/OpportunityListScreenshotTest.kt | 2 +- .../commcare/l10n/ProfileScreenshotTests.kt | 4 +- .../l10n/WorkHistoryScreenshotTest.kt | 2 +- 8 files changed, 79 insertions(+), 67 deletions(-) diff --git a/app/unit-tests/src/org/commcare/l10n/BaseConnectScreenshotTest.kt b/app/unit-tests/src/org/commcare/l10n/BaseConnectScreenshotTest.kt index 1506008cad..340d4ad339 100644 --- a/app/unit-tests/src/org/commcare/l10n/BaseConnectScreenshotTest.kt +++ b/app/unit-tests/src/org/commcare/l10n/BaseConnectScreenshotTest.kt @@ -49,7 +49,7 @@ abstract class BaseConnectScreenshotTest( ConnectTestUtils.createConnectDbFile() ConnectUserDatabaseUtil.storeUser( - ConnectScreenshotData.user(hasConnectAccess = true).apply { + ConnectScreenshotData.buildUser(hasConnectAccess = true).apply { updateConnectToken("showcase-connect-token", ConnectTestUtils.daysFromNow(1)) }, ) diff --git a/app/unit-tests/src/org/commcare/l10n/ChannelListScreenshotTest.kt b/app/unit-tests/src/org/commcare/l10n/ChannelListScreenshotTest.kt index fb08a18470..55557ad508 100644 --- a/app/unit-tests/src/org/commcare/l10n/ChannelListScreenshotTest.kt +++ b/app/unit-tests/src/org/commcare/l10n/ChannelListScreenshotTest.kt @@ -21,8 +21,8 @@ class ChannelListScreenshotTest( @Before fun seedMessaging() { val context = ApplicationProvider.getApplicationContext() - ConnectScreenshotData.channels().forEach { ConnectMessagingDatabaseHelper.storeMessagingChannel(context, it) } - ConnectScreenshotData.messages().forEach { ConnectMessagingDatabaseHelper.storeMessagingMessage(context, it) } + ConnectScreenshotData.buildChannels().forEach { ConnectMessagingDatabaseHelper.storeMessagingChannel(context, it) } + ConnectScreenshotData.buildMessages().forEach { ConnectMessagingDatabaseHelper.storeMessagingMessage(context, it) } } @Test diff --git a/app/unit-tests/src/org/commcare/l10n/ChannelMessagesScreenshotTest.kt b/app/unit-tests/src/org/commcare/l10n/ChannelMessagesScreenshotTest.kt index f8b2b62068..9783432cb3 100644 --- a/app/unit-tests/src/org/commcare/l10n/ChannelMessagesScreenshotTest.kt +++ b/app/unit-tests/src/org/commcare/l10n/ChannelMessagesScreenshotTest.kt @@ -25,8 +25,8 @@ class ChannelMessagesScreenshotTest( @Before fun seedMessaging() { val context = ApplicationProvider.getApplicationContext() - ConnectScreenshotData.channels().forEach { ConnectMessagingDatabaseHelper.storeMessagingChannel(context, it) } - ConnectScreenshotData.messages().forEach { ConnectMessagingDatabaseHelper.storeMessagingMessage(context, it) } + ConnectScreenshotData.buildChannels().forEach { ConnectMessagingDatabaseHelper.storeMessagingChannel(context, it) } + ConnectScreenshotData.buildMessages().forEach { ConnectMessagingDatabaseHelper.storeMessagingMessage(context, it) } } @Test diff --git a/app/unit-tests/src/org/commcare/l10n/ConnectScreenshotData.kt b/app/unit-tests/src/org/commcare/l10n/ConnectScreenshotData.kt index 3725744a67..7b55a20705 100644 --- a/app/unit-tests/src/org/commcare/l10n/ConnectScreenshotData.kt +++ b/app/unit-tests/src/org/commcare/l10n/ConnectScreenshotData.kt @@ -33,7 +33,7 @@ object ConnectScreenshotData { const val NEW_OPPORTUNITY_ID = 4 /** A PersonalID user with a full name, a Kenyan phone number and an email. */ - fun user(hasConnectAccess: Boolean = false): ConnectUserRecord = + fun buildUser(hasConnectAccess: Boolean = false): ConnectUserRecord = ConnectUserRecord( "+254712345678", "showcase-user-id", @@ -50,41 +50,41 @@ object ConnectScreenshotData { } /** One opportunity per state the opportunity list shows, with a long title and an expiry warning among them. */ - fun opportunities(): List = + fun buildOpportunities(): List = listOf( - opportunity( + buildOpportunity( LEARNING_OPPORTUNITY_ID, "Household Malaria Prevention Survey", ConnectJobRecord.STATUS_LEARNING, endInDays = 21, modulesDone = LEARNING_MODULES_DONE, ), - opportunity( + buildOpportunity( DELIVERING_OPPORTUNITY_ID, "Child Growth Monitoring and Nutrition Counselling, Kisumu County", ConnectJobRecord.STATUS_DELIVERING, endInDays = 2, visitsDone = DELIVERY_VISITS, ), - opportunity(3, "Maternal Health Home Visits", ConnectJobRecord.STATUS_DELIVERING, endInDays = 60, visitsDone = 12), - opportunity( + buildOpportunity(3, "Maternal Health Home Visits", ConnectJobRecord.STATUS_DELIVERING, endInDays = 60, visitsDone = 12), + buildOpportunity( NEW_OPPORTUNITY_ID, "Community Vaccination Awareness Campaign", ConnectJobRecord.STATUS_AVAILABLE_NEW, endInDays = 45, ), - opportunity(5, "Polio Campaign Follow-up", ConnectJobRecord.STATUS_DELIVERING, endInDays = -14, visitsDone = MAX_VISITS), - opportunity(6, "Bed Net Distribution Check", ConnectJobRecord.STATUS_DELIVERING, endInDays = -40, visitsDone = 18), + buildOpportunity(5, "Polio Campaign Follow-up", ConnectJobRecord.STATUS_DELIVERING, endInDays = -14, visitsDone = MAX_VISITS), + buildOpportunity(6, "Bed Net Distribution Check", ConnectJobRecord.STATUS_DELIVERING, endInDays = -40, visitsDone = 18), ) /** The opportunity UUID the server and the local database key the opportunity with [id] by. */ - fun opportunityUuid(id: Int): String = "showcase-opportunity-$id" + fun getOpportunityUuid(id: Int): String = "showcase-opportunity-$id" /** * The learn progress response for the opportunity with [id]: the learning opportunity is part way through its * modules, and every other one has finished them and passed the assessment. */ - fun learningProgressResponse(id: Int): String { + fun buildLearningProgressJson(id: Int): String { val modulesDone = if (id == LEARNING_OPPORTUNITY_ID) LEARNING_MODULES_DONE else LEARN_MODULES return JSONObject() .put( @@ -94,7 +94,7 @@ object ConnectScreenshotData { put( JSONObject().apply { put("id", module) - put("date", isoDateTime(minutesAgo = (40 - module * 6) * 24 * 60)) + put("date", formatIsoDateTime(minutesAgo = (40 - module * 6) * 24 * 60)) put("module", module) put("duration", "01:45:00") }, @@ -107,7 +107,7 @@ object ConnectScreenshotData { if (modulesDone == LEARN_MODULES) { put( JSONObject().apply { - put("date", isoDateTime(minutesAgo = 9 * 24 * 60)) + put("date", formatIsoDateTime(minutesAgo = 9 * 24 * 60)) put("score", 92) put("passing_score", 80) put("passed", true) @@ -122,7 +122,7 @@ object ConnectScreenshotData { * The delivery progress response for the delivering opportunity: its visits spread over the last week, the newest * of them still pending and one rejected, and two confirmed payments ahead of one still to confirm. */ - fun deliveryProgressResponse(): String { + fun buildDeliveryProgressJson(): String { val rejectedVisit = 7 val pendingVisits = 2 val approvedVisits = DELIVERY_VISITS - pendingVisits - 1 @@ -140,54 +140,54 @@ object ConnectScreenshotData { visit == rejectedVisit -> "rejected" else -> "approved" } - put(delivery(visit, status, minutesAgo = (visit / VISITS_PER_DAY) * 24 * 60 + 30 + visit * 20)) + put(buildDeliveryJson(visit, status, minutesAgo = (visit / VISITS_PER_DAY) * 24 * 60 + 30 + visit * 20)) } }, ).put( "payments", JSONArray().apply { - put(payment("1", visits = 12, daysAgo = 14, confirmed = true)) - put(payment("2", visits = 6, daysAgo = 7, confirmed = true)) - put(payment("3", visits = 5, daysAgo = 1, confirmed = false)) + put(buildPaymentJson("1", visits = 12, daysAgo = 14, confirmed = true)) + put(buildPaymentJson("2", visits = 6, daysAgo = 7, confirmed = true)) + put(buildPaymentJson("3", visits = 5, daysAgo = 1, confirmed = false)) }, ).toString() } /** Subscribed channels with unread, read and sent last messages, plus one the worker has unsubscribed from. */ - fun channels(): List = + fun buildChannels(): List = listOf( - channel(CONVERSATION_CHANNEL_ID, ORGANIZATION, consented = true), - channel("showcase-channel-support", "Connect Support", consented = true), - channel("showcase-channel-malaria", "Household Malaria Prevention Survey", consented = true), - channel("showcase-channel-nutrition", "Nutrition Programme Updates", consented = false), + buildChannel(CONVERSATION_CHANNEL_ID, ORGANIZATION, consented = true), + buildChannel("showcase-channel-support", "Connect Support", consented = true), + buildChannel("showcase-channel-malaria", "Household Malaria Prevention Survey", consented = true), + buildChannel("showcase-channel-nutrition", "Nutrition Programme Updates", consented = false), ) /** A conversation in [CONVERSATION_CHANNEL_ID] ending in two unread messages, and a last message in each other subscribed channel. */ - fun messages(): List = + fun buildMessages(): List = listOf( - message( + buildMessage( CONVERSATION_CHANNEL_ID, 1, minutesAgo = 26 * 60, outgoing = false, "Welcome to the Maternal Health Home Visits programme! Reply here if you have any questions.", ), - message( + buildMessage( CONVERSATION_CHANNEL_ID, 2, minutesAgo = 25 * 60, outgoing = true, "Thank you. Do I need to visit every household on my list this week?", ), - message( + buildMessage( CONVERSATION_CHANNEL_ID, 3, minutesAgo = 24 * 60, outgoing = false, "No, aim for 8 visits a day. Start with the households marked as high priority, and record each visit before you leave the house.", ), - message(CONVERSATION_CHANNEL_ID, 4, minutesAgo = 50, outgoing = true, "Understood. I finished 6 visits today."), - message( + buildMessage(CONVERSATION_CHANNEL_ID, 4, minutesAgo = 50, outgoing = true, "Understood. I finished 6 visits today."), + buildMessage( CONVERSATION_CHANNEL_ID, 5, minutesAgo = 12, @@ -195,7 +195,7 @@ object ConnectScreenshotData { "Great work! Your payment for last week has been approved.", viewed = false, ), - message( + buildMessage( CONVERSATION_CHANNEL_ID, 6, minutesAgo = 10, @@ -203,12 +203,24 @@ object ConnectScreenshotData { "Remember that the programme ends on Friday.", viewed = false, ), - message("showcase-channel-support", 1, minutesAgo = 30 * 60, outgoing = true, "My photo upload keeps failing, can you help?"), - message("showcase-channel-malaria", 1, minutesAgo = 6 * 24 * 60, outgoing = false, "The training modules are now available."), + buildMessage( + "showcase-channel-support", + 1, + minutesAgo = 30 * 60, + outgoing = true, + "My photo upload keeps failing, can you help?", + ), + buildMessage( + "showcase-channel-malaria", + 1, + minutesAgo = 6 * 24 * 60, + outgoing = false, + "The training modules are now available.", + ), ) /** The credentials response for Work History: one entry per activity level, newest first once sorted. */ - fun workHistoryResponse(): String = + fun buildWorkHistoryJson(): String = JSONObject() .put( "credentials", @@ -237,7 +249,7 @@ object ConnectScreenshotData { }, ).toString() - private fun channel( + private fun buildChannel( channelId: String, name: String, consented: Boolean, @@ -256,7 +268,7 @@ object ConnectScreenshotData { key = "showcase-key" } - private fun message( + private fun buildMessage( channelId: String, index: Int, minutesAgo: Int, @@ -274,7 +286,7 @@ object ConnectScreenshotData { userViewed = viewed } - private fun opportunity( + private fun buildOpportunity( id: Int, title: String, status: Int, @@ -282,11 +294,11 @@ object ConnectScreenshotData { modulesDone: Int = LEARN_MODULES, visitsDone: Int = 0, ): ConnectJobRecord = - ConnectJobRecord.fromJson(jobJson(id, title, endInDays, modulesDone, visitsDone)).apply { + ConnectJobRecord.fromJson(buildJobJson(id, title, endInDays, modulesDone, visitsDone)).apply { this.status = status } - private fun jobJson( + private fun buildJobJson( id: Int, title: String, endInDays: Int, @@ -295,20 +307,20 @@ object ConnectScreenshotData { ): JSONObject = JSONObject().apply { put("id", id) - put("opportunity_id", opportunityUuid(id)) + put("opportunity_id", getOpportunityUuid(id)) put("name", title) put("description", "Visit households in your area, record what you find and share health guidance with each family.") put("short_description", "Visit households and record what you find.") put("organization", ORGANIZATION) - put("start_date", isoDate(endInDays - 120)) - put("end_date", isoDate(endInDays)) + put("start_date", formatIsoDate(endInDays - 120)) + put("end_date", formatIsoDate(endInDays)) put("max_visits_per_user", MAX_VISITS) put("daily_max_visits_per_user", 8) put("budget_per_visit", BUDGET_PER_VISIT) put("budget_per_user", BUDGET_PER_VISIT * MAX_VISITS) put("currency", CURRENCY) put("deliver_progress", visitsDone) - put("payment_units", paymentUnitsJson()) + put("payment_units", buildPaymentUnitsJson()) put( "learn_progress", JSONObject().apply { @@ -316,11 +328,11 @@ object ConnectScreenshotData { put("completed_modules", modulesDone) }, ) - put("learn_app", appJson("showcase-learn-$id", withModules = true)) - put("deliver_app", appJson("showcase-deliver-$id", withModules = false)) + put("learn_app", buildAppJson("showcase-learn-$id", withModules = true)) + put("deliver_app", buildAppJson("showcase-deliver-$id", withModules = false)) } - private fun paymentUnitsJson(): JSONArray = + private fun buildPaymentUnitsJson(): JSONArray = JSONArray().put( JSONObject().apply { put("id", 1) @@ -332,14 +344,14 @@ object ConnectScreenshotData { }, ) - private fun delivery( + private fun buildDeliveryJson( visit: Int, status: String, minutesAgo: Int, ): JSONObject = JSONObject().apply { put("id", visit + 1) - put("visit_date", isoDateTime(minutesAgo)) + put("visit_date", formatIsoDateTime(minutesAgo)) put("status", status) put("deliver_unit_name", "Household visit") put("deliver_unit_slug", PAYMENT_UNIT_ID) @@ -349,7 +361,7 @@ object ConnectScreenshotData { put("reason", if (status == "rejected") "The photo of the growth chart is missing." else "") } - private fun payment( + private fun buildPaymentJson( id: String, visits: Int, daysAgo: Int, @@ -358,15 +370,15 @@ object ConnectScreenshotData { JSONObject().apply { put("id", id) put("payment_id", "showcase-payment-$id") - put("date_paid", isoDateTime(minutesAgo = daysAgo * 24 * 60)) + put("date_paid", formatIsoDateTime(minutesAgo = daysAgo * 24 * 60)) put("amount", visits * BUDGET_PER_VISIT) put("confirmed", confirmed) if (confirmed) { - put("date_confirmed", isoDate(-daysAgo + 1)) + put("date_confirmed", formatIsoDate(-daysAgo + 1)) } } - private fun appJson( + private fun buildAppJson( appId: String, withModules: Boolean, ): JSONObject = @@ -378,10 +390,10 @@ object ConnectScreenshotData { put("organization", ORGANIZATION) put("passing_score", 80) put("install_url", "https://example.org/$appId.ccz") - put("learn_modules", if (withModules) modulesJson() else JSONArray()) + put("learn_modules", if (withModules) buildModulesJson() else JSONArray()) } - private fun modulesJson(): JSONArray = + private fun buildModulesJson(): JSONArray = JSONArray().apply { listOf("Introduction to Connect", "Talking with Households", "Recognising Danger Signs", "Recording a Visit", "Referrals") .forEachIndexed { index, name -> @@ -397,10 +409,10 @@ object ConnectScreenshotData { } } - private fun isoDateTime(minutesAgo: Int): String = + private fun formatIsoDateTime(minutesAgo: Int): String = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US) .format(Date(System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(minutesAgo.toLong()))) - private fun isoDate(daysFromNow: Int): String = + private fun formatIsoDate(daysFromNow: Int): String = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, daysFromNow) }.time) } diff --git a/app/unit-tests/src/org/commcare/l10n/OpportunityHomeScreenshotTest.kt b/app/unit-tests/src/org/commcare/l10n/OpportunityHomeScreenshotTest.kt index a439289039..afee93dfa7 100644 --- a/app/unit-tests/src/org/commcare/l10n/OpportunityHomeScreenshotTest.kt +++ b/app/unit-tests/src/org/commcare/l10n/OpportunityHomeScreenshotTest.kt @@ -32,7 +32,7 @@ class OpportunityHomeScreenshotTest( ) : BaseConnectScreenshotTest(target) { @Before fun seedOpportunities() { - ConnectJobUtils.storeJobs(ApplicationProvider.getApplicationContext(), ConnectScreenshotData.opportunities(), true) + ConnectJobUtils.storeJobs(ApplicationProvider.getApplicationContext(), ConnectScreenshotData.buildOpportunities(), true) ConnectSyncPreferences.getInstance().apply { markSessionStart() storeLastSyncTime(ConnectRepository.SYNC_KEY_OPPORTUNITIES) @@ -48,11 +48,11 @@ class OpportunityHomeScreenshotTest( ?.toIntOrNull() ?: return "{}" return when { path.endsWith("/learn_progress") -> { - ConnectScreenshotData.learningProgressResponse(opportunityId) + ConnectScreenshotData.buildLearningProgressJson(opportunityId) } path.endsWith("/delivery_progress") && opportunityId == ConnectScreenshotData.DELIVERING_OPPORTUNITY_ID -> { - ConnectScreenshotData.deliveryProgressResponse() + ConnectScreenshotData.buildDeliveryProgressJson() } else -> { @@ -117,7 +117,7 @@ class OpportunityHomeScreenshotTest( val intent = Intent(ApplicationProvider.getApplicationContext(), ConnectActivity::class.java).apply { putExtra(ConnectConstants.GO_TO_JOB_STATUS, true) - putExtra(ConnectConstants.OPPORTUNITY_UUID, ConnectScreenshotData.opportunityUuid(opportunityId)) + putExtra(ConnectConstants.OPPORTUNITY_UUID, ConnectScreenshotData.getOpportunityUuid(opportunityId)) } val controller = Robolectric diff --git a/app/unit-tests/src/org/commcare/l10n/OpportunityListScreenshotTest.kt b/app/unit-tests/src/org/commcare/l10n/OpportunityListScreenshotTest.kt index 54eadfadba..ac3afd8550 100644 --- a/app/unit-tests/src/org/commcare/l10n/OpportunityListScreenshotTest.kt +++ b/app/unit-tests/src/org/commcare/l10n/OpportunityListScreenshotTest.kt @@ -22,7 +22,7 @@ class OpportunityListScreenshotTest( ) : BaseConnectScreenshotTest(target) { @Before fun seedOpportunities() { - ConnectJobUtils.storeJobs(ApplicationProvider.getApplicationContext(), ConnectScreenshotData.opportunities(), true) + ConnectJobUtils.storeJobs(ApplicationProvider.getApplicationContext(), ConnectScreenshotData.buildOpportunities(), true) ConnectSyncPreferences.getInstance().apply { markSessionStart() storeLastSyncTime(ConnectRepository.SYNC_KEY_OPPORTUNITIES) diff --git a/app/unit-tests/src/org/commcare/l10n/ProfileScreenshotTests.kt b/app/unit-tests/src/org/commcare/l10n/ProfileScreenshotTests.kt index 49b7f5c449..b62eb54622 100644 --- a/app/unit-tests/src/org/commcare/l10n/ProfileScreenshotTests.kt +++ b/app/unit-tests/src/org/commcare/l10n/ProfileScreenshotTests.kt @@ -21,7 +21,7 @@ class ProfileScreenshotTest( @get:Rule val localeRule: TestRule = L10nScreenshots.localeRule(target) - override fun buildUser(): ConnectUserRecord = ConnectScreenshotData.user() + override fun buildUser(): ConnectUserRecord = ConnectScreenshotData.buildUser() @Test fun profile() = L10nScreenshots.capture(activityController, target, "40_personalid_profile") @@ -42,7 +42,7 @@ class ProfileEditScreenshotTest( @get:Rule val localeRule: TestRule = L10nScreenshots.localeRule(target) - override fun buildUser(): ConnectUserRecord = ConnectScreenshotData.user() + override fun buildUser(): ConnectUserRecord = ConnectScreenshotData.buildUser() @Test fun profileEdit() { diff --git a/app/unit-tests/src/org/commcare/l10n/WorkHistoryScreenshotTest.kt b/app/unit-tests/src/org/commcare/l10n/WorkHistoryScreenshotTest.kt index cf827c1c71..b8139fe208 100644 --- a/app/unit-tests/src/org/commcare/l10n/WorkHistoryScreenshotTest.kt +++ b/app/unit-tests/src/org/commcare/l10n/WorkHistoryScreenshotTest.kt @@ -25,7 +25,7 @@ class WorkHistoryScreenshotTest( @Before fun serveWorkHistory() { personalIdApi.start() - personalIdApi.server.enqueue(MockResponse().setResponseCode(200).setBody(ConnectScreenshotData.workHistoryResponse())) + personalIdApi.server.enqueue(MockResponse().setResponseCode(200).setBody(ConnectScreenshotData.buildWorkHistoryJson())) } @After