diff --git a/RELEASES.md b/RELEASES.md index 6d1c61fbd3..7c1febd7e0 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -12,6 +12,7 @@ This file is meant as an easy way for us to collate notes and change logs across - PersonalID sign-up, account recovery and profile screens now share consistent headings, text styles, spacing and colors. - Messaging and Work History now show the sidebar, and opening another sidebar section replaces the current one instead of stacking on top of it. - After an incorrect email or phone OTP code, PersonalID now shows how many attempts remain before a new code must be requested. +- Opportunities whose visits have no daily limit now show today's visit count without a daily progress bar or "daily limit reached" warning. #### Important Bug Fixes @@ -38,6 +39,9 @@ This file is meant as an easy way for us to collate notes and change logs across - Enter wrong email OTP codes during sign-up, profile email edit, and forgot-backup-code recovery, and confirm the error shows the attempts remaining (2, then 1) before the "request a new code" message appears. - For an invited user receiving the phone OTP by SMS through PersonalID, confirm a wrong code likewise shows the attempts remaining. - Gregorian Date Widget: set a date in portrait, rotate to landscape, and update it with the keyboard. Tapping the day or the year field should bring up the keyboard's own full-width editor with a DONE key, and the value typed there should apply to the widget once DONE is pressed. +- On an opportunity with no daily visit limit, confirm the delivery dashboard and the job tile on the app home screen show only today's visit count with no progress bar, and no daily-limit warning appears. + - Confirm the opportunity intro and learning-complete screens omit the "Up to N per day" text for such opportunities. + - On an opportunity that still has daily limits, confirm the daily progress bar and daily-limit warnings work as before. ## CommCare 2.64.1 diff --git a/app/res/layout/item_progress_job_summary_visit.xml b/app/res/layout/item_progress_job_summary_visit.xml index 6739ef0517..9f6c591c81 100644 --- a/app/res/layout/item_progress_job_summary_visit.xml +++ b/app/res/layout/item_progress_job_summary_visit.xml @@ -24,8 +24,10 @@ tools:text="2/2" android:textColor="@color/connect_dark_blue_color" android:textSize="14sp" + android:layout_marginEnd="20dp" + app:layout_goneMarginEnd="0dp" app:layout_constraintBottom_toBottomOf="@+id/tv_primary_visit_title" - app:layout_constraintEnd_toStartOf="@+id/guideline3" + app:layout_constraintEnd_toStartOf="@+id/lp_primary_visit_progress" app:layout_constraintTop_toTopOf="@+id/tv_primary_visit_title" /> diff --git a/app/src/org/commcare/activities/StandardHomeActivityUIController.java b/app/src/org/commcare/activities/StandardHomeActivityUIController.java index 4e537b7312..5c8aadd7e8 100644 --- a/app/src/org/commcare/activities/StandardHomeActivityUIController.java +++ b/app/src/org/commcare/activities/StandardHomeActivityUIController.java @@ -234,7 +234,9 @@ public void updateConnectJobProgress() { list.add(new ConnectDeliveryPaymentSummaryInfo( activity.getString(R.string.connect_job_tile_daily_visits), job.numberOfDeliveriesToday(), - job.getMaxDailyVisits() + job.hasDailyLimit() + ? job.getMaxDailyVisits() + : ConnectDeliveryPaymentSummaryInfo.NO_DAILY_LIMIT )); connectProgressJobSummaryAdapter.setDeliverySummaries(list); diff --git a/app/src/org/commcare/adapters/ConnectProgressJobSummaryAdapter.java b/app/src/org/commcare/adapters/ConnectProgressJobSummaryAdapter.java index 234af95402..a45cc9c228 100644 --- a/app/src/org/commcare/adapters/ConnectProgressJobSummaryAdapter.java +++ b/app/src/org/commcare/adapters/ConnectProgressJobSummaryAdapter.java @@ -36,6 +36,15 @@ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public void onBindViewHolder(@NonNull ViewHolder holder, int position) { ConnectDeliveryPaymentSummaryInfo summary = deliverySummaries.get(position); holder.tvPrimaryVisitTitle.setText(summary.getPaymentUnitName()); + if (!summary.hasDailyLimit()) { + holder.lpPrimaryVisitProgress.setVisibility(View.GONE); + holder.tvPrimaryVisitCount.setText( + String.format(Locale.getDefault(), "%d", summary.getPaymentUnitAmount()) + ); + return; + } + + holder.lpPrimaryVisitProgress.setVisibility(View.VISIBLE); holder.tvPrimaryVisitCount.setText(String.format(Locale.getDefault(), "%d/%d", summary.getPaymentUnitAmount(), summary.getPaymentUnitMaxDaily())); diff --git a/app/src/org/commcare/android/database/connect/models/ConnectDeliveryPaymentSummaryInfo.java b/app/src/org/commcare/android/database/connect/models/ConnectDeliveryPaymentSummaryInfo.java index 9f36b30630..fff7c5d331 100644 --- a/app/src/org/commcare/android/database/connect/models/ConnectDeliveryPaymentSummaryInfo.java +++ b/app/src/org/commcare/android/database/connect/models/ConnectDeliveryPaymentSummaryInfo.java @@ -1,6 +1,8 @@ package org.commcare.android.database.connect.models; public class ConnectDeliveryPaymentSummaryInfo { + public static final int NO_DAILY_LIMIT = -1; + private String paymentUnitName; private int paymentUnitAmount; private int paymentUnitMaxDaily; @@ -35,4 +37,8 @@ public int getPaymentUnitMaxDaily() { public void setPaymentUnitMaxDaily(int paymentUnitMaxDaily) { this.paymentUnitMaxDaily = paymentUnitMaxDaily; } + + public boolean hasDailyLimit() { + return paymentUnitMaxDaily != NO_DAILY_LIMIT; + } } diff --git a/app/src/org/commcare/android/database/connect/models/ConnectJobRecord.java b/app/src/org/commcare/android/database/connect/models/ConnectJobRecord.java index 8e968e084a..ec13a4eebe 100644 --- a/app/src/org/commcare/android/database/connect/models/ConnectJobRecord.java +++ b/app/src/org/commcare/android/database/connect/models/ConnectJobRecord.java @@ -91,6 +91,7 @@ public class ConnectJobRecord extends Persisted implements Serializable { public static final String META_JOB_UUID = "opportunity_id"; + public static final int NO_DAILY_LIMIT = -1; @Persisting(1) @MetaField(META_JOB_ID) @@ -205,7 +206,7 @@ public static ConnectJobRecord fromJson(JSONObject json) throws JSONException { job.projectEndDate = JsonExtensions.requireDate(json, META_END_DATE); job.projectStartDate = JsonExtensions.requireDate(json, META_START_DATE); job.maxVisits = json.getInt(META_MAX_VISITS_PER_USER); - job.maxDailyVisits = json.getInt(META_MAX_DAILY_VISITS); + job.maxDailyVisits = JsonExtensions.optIntSafe(json, META_MAX_DAILY_VISITS, NO_DAILY_LIMIT); job.budgetPerVisit = json.getInt(META_BUDGET_PER_VISIT); String budgetPerUserKey = "budget_per_user"; job.totalBudget = json.getInt(budgetPerUserKey); @@ -347,6 +348,14 @@ public int getMaxDailyVisits() { return maxDailyVisits; } + public boolean hasDailyLimit() { + return maxDailyVisits != NO_DAILY_LIMIT; + } + + private boolean isDailyLimitReached() { + return hasDailyLimit() && numberOfDeliveriesToday() >= maxDailyVisits; + } + public Date getProjectStartDate() { return projectStartDate; } @@ -778,7 +787,7 @@ public String getCardMessageText(Context context) { // The job-level caps are checked ahead of the per-unit warnings: once the whole // opportunity is spent, which individual unit ran out first no longer matters. return context.getString(R.string.connect_progress_warning_max_reached_single); - } else if (numberOfDeliveriesToday() >= getMaxDailyVisits()) { + } else if (isDailyLimitReached()) { return context.getString(R.string.connect_progress_warning_daily_max_reached_single); } else if (!getPaymentUnits().isEmpty()) { return getMultiVisitWarnings(context); @@ -802,7 +811,7 @@ private String getMultiVisitWarnings(Context context) { totalMaxes.add(unit.getName()); } else { int todayCount = today.containsKey(key) ? today.get(key) : 0; - if (todayCount >= unit.getMaxDaily()) { + if (unit.isDailyLimitReached(todayCount)) { dailyMaxes.add(unit.getName()); } } @@ -841,8 +850,7 @@ public boolean isFurtherWorkBlocked() { } // The job-level caps bind whatever the payment units allow, so they are checked first. - if (getDeliveries().size() >= getMaxVisits() - || numberOfDeliveriesToday() >= getMaxDailyVisits()) { + if (getDeliveries().size() >= getMaxVisits() || isDailyLimitReached()) { return true; } @@ -866,7 +874,7 @@ public Set getPaymentUnitsAtLimit() { String key = unit.getUnitUUID(); int totalCount = total.containsKey(key) ? total.get(key) : 0; int todayCount = today.containsKey(key) ? today.get(key) : 0; - if (totalCount >= unit.getMaxTotal() || todayCount >= unit.getMaxDaily()) { + if (totalCount >= unit.getMaxTotal() || unit.isDailyLimitReached(todayCount)) { atLimit.add(key); } } diff --git a/app/src/org/commcare/android/database/connect/models/ConnectPaymentUnitRecord.java b/app/src/org/commcare/android/database/connect/models/ConnectPaymentUnitRecord.java index d2ca6df301..d3fad7e54b 100644 --- a/app/src/org/commcare/android/database/connect/models/ConnectPaymentUnitRecord.java +++ b/app/src/org/commcare/android/database/connect/models/ConnectPaymentUnitRecord.java @@ -4,6 +4,7 @@ import org.commcare.models.framework.Persisting; import org.commcare.modern.database.Table; import org.commcare.modern.models.MetaField; +import org.commcare.utils.JsonExtensions; import org.javarosa.core.services.Logger; import org.json.JSONException; import org.json.JSONObject; @@ -28,6 +29,8 @@ public class ConnectPaymentUnitRecord extends Persisted implements Serializable public static final String META_JOB_UUID = ConnectJobRecord.META_JOB_UUID; public static final String META_PAYMENT_UNIT_UUID = "payment_unit_id"; + public static final int NO_DAILY_LIMIT = -1; + @Persisting(1) @MetaField(META_JOB_ID) private int jobId; @@ -76,7 +79,7 @@ public static ConnectPaymentUnitRecord fromJson(JSONObject json, ConnectJobRecor paymentUnit.name = json.getString(META_NAME); paymentUnit.maxTotal = json.getInt(META_TOTAL); - paymentUnit.maxDaily = json.getInt(META_DAILY); + paymentUnit.maxDaily = JsonExtensions.optIntSafe(json, META_DAILY, NO_DAILY_LIMIT); paymentUnit.amount = json.getInt(META_AMOUNT); return paymentUnit; @@ -123,6 +126,14 @@ public int getMaxDaily() { return maxDaily; } + public boolean hasDailyLimit() { + return maxDaily != NO_DAILY_LIMIT; + } + + public boolean isDailyLimitReached(int visitsToday) { + return hasDailyLimit() && visitsToday >= maxDaily; + } + public int getAmount() { return amount; } diff --git a/app/src/org/commcare/connect/network/connect/ConnectNetworkClient.kt b/app/src/org/commcare/connect/network/connect/ConnectNetworkClient.kt index 9338044082..4951f4442e 100644 --- a/app/src/org/commcare/connect/network/connect/ConnectNetworkClient.kt +++ b/app/src/org/commcare/connect/network/connect/ConnectNetworkClient.kt @@ -29,7 +29,7 @@ class ConnectNetworkClient ) { companion object { private const val BASE_URL = "https://${BuildConfig.CCC_HOST}" - private const val API_VERSION_CONNECT = "1.0" + private const val API_VERSION_CONNECT = "2.0" @Volatile private var instance: ConnectNetworkClient? = null diff --git a/app/src/org/commcare/fragments/connect/ConnectDeliveryDashboardFragment.kt b/app/src/org/commcare/fragments/connect/ConnectDeliveryDashboardFragment.kt index 81fe47766b..4fc99aa98f 100644 --- a/app/src/org/commcare/fragments/connect/ConnectDeliveryDashboardFragment.kt +++ b/app/src/org/commcare/fragments/connect/ConnectDeliveryDashboardFragment.kt @@ -62,8 +62,8 @@ class ConnectDeliveryDashboardFragment : } private fun bindVisitProgress(contentEnabled: Boolean) { - val doneToday = job.numberOfDeliveriesToday() - val remainingToday = (job.maxDailyVisits - doneToday).coerceAtLeast(0) + val visitsDoneToday = job.numberOfDeliveriesToday() + val maxDailyVisits = job.maxDailyVisits.takeIf { job.hasDailyLimit() } val cardMessage: String? = job.getCardMessageText(requireContext()) binding.deliveryProgressCard.bind( ConnectProgressCard.State( @@ -85,14 +85,17 @@ class ConnectDeliveryDashboardFragment : linearProgress = ConnectProgressCard.State.LinearProgress( label = getString(R.string.connect_delivery_daily_visits), - current = doneToday, - max = job.maxDailyVisits, + current = visitsDoneToday, + max = maxDailyVisits, caption = - resources.getQuantityString( - R.plurals.connect_delivery_visits_remaining_today, - remainingToday, - remainingToday, - ), + maxDailyVisits?.let { max -> + val remainingToday = (max - visitsDoneToday).coerceAtLeast(0) + resources.getQuantityString( + R.plurals.connect_delivery_visits_remaining_today, + remainingToday, + remainingToday, + ) + }, ), ), ) diff --git a/app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.kt b/app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.kt index eb5dea018d..e3e99ae76b 100644 --- a/app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.kt +++ b/app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.kt @@ -158,7 +158,7 @@ class ConnectJobIntroFragment : ConnectJobFragment 0) { diff --git a/app/unit-tests/src/org/commcare/android/database/connect/models/ConnectJobRecordCardMessageTest.kt b/app/unit-tests/src/org/commcare/android/database/connect/models/ConnectJobRecordCardMessageTest.kt index 532117638a..f599b3c7a3 100644 --- a/app/unit-tests/src/org/commcare/android/database/connect/models/ConnectJobRecordCardMessageTest.kt +++ b/app/unit-tests/src/org/commcare/android/database/connect/models/ConnectJobRecordCardMessageTest.kt @@ -71,7 +71,7 @@ class ConnectJobRecordCardMessageTest { private fun paymentUnit( id: Int, maxTotal: Int, - maxDaily: Int, + maxDaily: Int?, ): ConnectPaymentUnitRecord = ConnectPaymentUnitRecord.fromJson( JSONObject( @@ -155,6 +155,35 @@ class ConnectJobRecordCardMessageTest { ) } + @Test + fun `a job without a daily limit never reports the daily maximum`() { + val job = multiPaymentJob(maxDailyVisits = ConnectJobRecord.NO_DAILY_LIMIT) + job.deliveries = List(5) { delivery(it, 1, Date()) } + + assertNull(job.getCardMessageText(context)) + } + + @Test + fun `a payment unit without a daily limit is never named in the daily maximum warning`() { + val job = multiPaymentJob() + job.paymentUnits = + listOf( + paymentUnit(id = 1, maxTotal = 50, maxDaily = null), + paymentUnit(id = 2, maxTotal = 50, maxDaily = 1), + ) + job.deliveries = + listOf( + delivery(1, 1, Date()), + delivery(2, 1, Date()), + delivery(3, 2, Date()), + ) + + assertEquals( + context.getString(R.string.connect_progress_warning_daily_max_reached_multi, "Unit 2"), + job.getCardMessageText(context), + ) + } + @Test fun `a multi-payment job with room everywhere reports nothing`() { val job = multiPaymentJob() diff --git a/app/unit-tests/src/org/commcare/android/database/connect/models/ConnectJobRecordWorkBlockedTest.kt b/app/unit-tests/src/org/commcare/android/database/connect/models/ConnectJobRecordWorkBlockedTest.kt index edec9ba3e3..7295c580b3 100644 --- a/app/unit-tests/src/org/commcare/android/database/connect/models/ConnectJobRecordWorkBlockedTest.kt +++ b/app/unit-tests/src/org/commcare/android/database/connect/models/ConnectJobRecordWorkBlockedTest.kt @@ -38,7 +38,7 @@ class ConnectJobRecordWorkBlockedTest { private fun paymentUnit( id: Int, maxTotal: Int, - maxDaily: Int, + maxDaily: Int?, ): ConnectPaymentUnitRecord = ConnectPaymentUnitRecord.fromJson( JSONObject( @@ -300,6 +300,37 @@ class ConnectJobRecordWorkBlockedTest { assertTrue(job.isFurtherWorkBlocked) } + @Test + fun `a job without a daily limit is not blocked by today's visits`() { + val job = job(maxVisits = 10, maxDailyVisits = ConnectJobRecord.NO_DAILY_LIMIT) + job.deliveries = List(5) { delivery(it, 1, Date()) } + + assertFalse(job.isFurtherWorkBlocked) + } + + @Test + fun `a payment unit without a daily limit is not at its limit from today's visits`() { + val job = job(maxVisits = 100, maxDailyVisits = ConnectJobRecord.NO_DAILY_LIMIT) + job.paymentUnits = listOf(paymentUnit(id = 1, maxTotal = 50, maxDaily = null)) + job.deliveries = List(5) { delivery(it, 1, Date()) } + + assertTrue(job.paymentUnitsAtLimit.isEmpty()) + assertFalse(job.isFurtherWorkBlocked) + } + + @Test + fun `a payment unit without a daily limit is still blocked by its total limit`() { + val job = job(maxVisits = 100, maxDailyVisits = ConnectJobRecord.NO_DAILY_LIMIT) + job.paymentUnits = listOf(paymentUnit(id = 1, maxTotal = 2, maxDaily = null)) + job.deliveries = + listOf( + delivery(1, 1, daysFromNow(-1)), + delivery(2, 1, daysFromNow(-1)), + ) + + assertTrue(job.isFurtherWorkBlocked) + } + /** Guards the empty case, where an unguarded `atLimit.size == units.size` compares 0 to 0. */ @Test fun `a job with no payment units is not blocked while it has visits left`() { diff --git a/app/unit-tests/src/org/commcare/connect/network/connect/parser/ConnectOpportunitiesParserTest.kt b/app/unit-tests/src/org/commcare/connect/network/connect/parser/ConnectOpportunitiesParserTest.kt index 8de5e47cf4..5affbd5210 100644 --- a/app/unit-tests/src/org/commcare/connect/network/connect/parser/ConnectOpportunitiesParserTest.kt +++ b/app/unit-tests/src/org/commcare/connect/network/connect/parser/ConnectOpportunitiesParserTest.kt @@ -19,7 +19,9 @@ import org.json.JSONException import org.json.JSONObject import org.junit.After import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -87,6 +89,22 @@ class ConnectOpportunitiesParserTest { put("learn_modules", JSONArray()) } + private fun paymentUnitJson(maxDaily: Any): JSONObject = + JSONObject().apply { + put("id", 7) + put("payment_unit_id", "unit-uuid-7") + put("name", "Registration") + put("max_total", 20) + put("max_daily", maxDaily) + put("amount", 100) + } + + private fun parseSingleJob(job: JSONObject): ConnectJobRecord { + every { ConnectJobUtils.storeJobs(any(), any(), any()) } returns 1 + every { ConnectReleaseTogglesWorker.scheduleOneTimeFetch(any()) } just Runs + return parser.parse(200, jsonArrayOf(job), null).single() + } + private fun claimedJobJson( id: Int, claim: JSONObject, @@ -237,6 +255,46 @@ class ConnectOpportunitiesParserTest { verify(exactly = 1) { ConnectJobUtils.storeJobs(any(), match { it.isEmpty() }, true) } } + @Test + fun `parse keeps the daily limits when the job and its payment unit have them`() { + val job = + parseSingleJob( + validJobJson(1).apply { put("payment_units", JSONArray().put(paymentUnitJson(5))) }, + ) + + assertTrue(job.hasDailyLimit()) + assertEquals(10, job.maxDailyVisits) + assertTrue(job.paymentUnits.single().hasDailyLimit()) + assertEquals(5, job.paymentUnits.single().maxDaily) + } + + @Test + fun `parse treats a null job daily limit as no daily limit`() { + val job = parseSingleJob(validJobJson(1).apply { put("daily_max_visits_per_user", JSONObject.NULL) }) + + assertFalse(job.hasDailyLimit()) + } + + @Test + fun `parse treats a null payment unit daily limit as no daily limit`() { + val job = + parseSingleJob( + validJobJson(1).apply { + put("payment_units", JSONArray().put(paymentUnitJson(JSONObject.NULL))) + }, + ) + + assertFalse(job.paymentUnits.single().hasDailyLimit()) + assertEquals(20, job.paymentUnits.single().maxTotal) + } + + @Test + fun `parse treats a missing job daily limit as no daily limit`() { + val job = parseSingleJob(validJobJson(1).apply { remove("daily_max_visits_per_user") }) + + assertFalse(job.hasDailyLimit()) + } + @Test fun `parse stores valid jobs when one entry has a null end_date`() { assertBadJobIsSkipped(validJobJson(10).apply { put("end_date", JSONObject.NULL) }) diff --git a/app/unit-tests/src/org/commcare/fragments/connect/ConnectDeliveryDashboardFragmentTest.kt b/app/unit-tests/src/org/commcare/fragments/connect/ConnectDeliveryDashboardFragmentTest.kt index f245e3d10b..55ff02c3a3 100644 --- a/app/unit-tests/src/org/commcare/fragments/connect/ConnectDeliveryDashboardFragmentTest.kt +++ b/app/unit-tests/src/org/commcare/fragments/connect/ConnectDeliveryDashboardFragmentTest.kt @@ -184,6 +184,17 @@ class ConnectDeliveryDashboardFragmentTest { ) } + @Test + fun `a job without a daily limit shows only today's visit count`() { + job = seedDeliveryJob(maxDailyVisits = ConnectJobRecord.NO_DAILY_LIMIT) + val view = launch(progressResponse(deliveries = deliveriesToday(unit = 1, count = 3))).requireView() + + assertEquals("3", view.findViewById(R.id.progress_card_bar_count).text.toString()) + assertEquals(View.GONE, view.findViewById(R.id.progress_card_linear_bar).visibility) + assertEquals(View.GONE, view.findViewById(R.id.progress_card_bar_caption).visibility) + assertEquals(View.GONE, view.findViewById(R.id.progress_card_info_message).visibility) + } + /** * The sync card is the in-page equivalent of the action bar's sync, so the click has to make the * same delivery-progress call and land its result on the figures. @@ -533,10 +544,11 @@ class ConnectDeliveryDashboardFragmentTest { * Writes the opportunity through the real storage layer so the repository's cache read and the * dashboard both see the same record production would. */ - private fun seedDeliveryJob(): ConnectJobRecord { + private fun seedDeliveryJob(maxDailyVisits: Int = ConnectLearnJobTestData.MAX_DAILY_VISITS): ConnectJobRecord { val seeded = ConnectLearnJobTestData.job().apply { status = ConnectJobRecord.STATUS_DELIVERING + setMaxDailyVisits(maxDailyVisits) } ConnectJobUtils.storeJobs(appContext, listOf(seeded), true) return ConnectJobUtils.getCompositeJob(ConnectLearnJobTestData.JOB_UUID)!! diff --git a/app/unit-tests/src/org/commcare/fragments/connect/ConnectJobIntroFragmentTest.kt b/app/unit-tests/src/org/commcare/fragments/connect/ConnectJobIntroFragmentTest.kt index f8a7a4c542..4af3f20afc 100644 --- a/app/unit-tests/src/org/commcare/fragments/connect/ConnectJobIntroFragmentTest.kt +++ b/app/unit-tests/src/org/commcare/fragments/connect/ConnectJobIntroFragmentTest.kt @@ -136,6 +136,15 @@ class ConnectJobIntroFragmentTest : BaseConnectJobIntroTest() { ) } + @Test + fun `max visits card drops the per-day subtitle when the job has no daily limit`() { + job.setMaxDailyVisits(ConnectJobRecord.NO_DAILY_LIMIT) + val fragment = launch() + + assertEquals("100", cardValue(fragment, R.id.card_max_visits)) + assertEquals("", cardSubtitle(fragment, R.id.card_max_visits)) + } + @Test fun `days card shows the days remaining`() { val fragment = launch() diff --git a/app/unit-tests/src/org/commcare/utils/JsonExtensionsTest.kt b/app/unit-tests/src/org/commcare/utils/JsonExtensionsTest.kt new file mode 100644 index 0000000000..a176b7c11b --- /dev/null +++ b/app/unit-tests/src/org/commcare/utils/JsonExtensionsTest.kt @@ -0,0 +1,37 @@ +package org.commcare.utils + +import org.json.JSONException +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Test + +class JsonExtensionsTest { + @Test + fun `optIntSafe returns the int when present`() { + val json = JSONObject().put("limit", 5) + + assertEquals(5, json.optIntSafe("limit", -1)) + } + + @Test + fun `optIntSafe returns the fallback when the value is null`() { + val json = JSONObject().put("limit", JSONObject.NULL) + + assertEquals(-1, json.optIntSafe("limit", -1)) + } + + @Test + fun `optIntSafe returns the fallback when the key is missing`() { + assertEquals(-1, JSONObject().optIntSafe("limit", -1)) + } + + @Test + fun `optIntSafe throws when the value is not a number`() { + val json = JSONObject().put("limit", "abc") + + assertThrows(JSONException::class.java) { + json.optIntSafe("limit", -1) + } + } +} diff --git a/app/unit-tests/src/org/commcare/views/connect/ConnectLearnCompleteViewTest.kt b/app/unit-tests/src/org/commcare/views/connect/ConnectLearnCompleteViewTest.kt index c9baafab02..cbbd6025fc 100644 --- a/app/unit-tests/src/org/commcare/views/connect/ConnectLearnCompleteViewTest.kt +++ b/app/unit-tests/src/org/commcare/views/connect/ConnectLearnCompleteViewTest.kt @@ -136,6 +136,15 @@ class ConnectLearnCompleteViewTest { ) } + @Test + fun `the visits card drops the per-day subtitle when the job has no daily limit`() { + val job = ConnectLearnJobTestData.job().apply { setMaxDailyVisits(ConnectJobRecord.NO_DAILY_LIMIT) } + val view = bind(job) + + assertEquals(job.maxPossibleVisits.toString(), view.card(R.id.card_total_visits).valueText) + assertTrue(view.card(R.id.card_total_visits).subtitleText.isNullOrEmpty()) + } + @Test fun `cta offers the delivery download when the delivery app is missing`() { val view = bind() diff --git a/app/unit-tests/src/org/commcare/views/connect/ConnectProgressCardTest.kt b/app/unit-tests/src/org/commcare/views/connect/ConnectProgressCardTest.kt index b710f7da1e..43591fc1ef 100644 --- a/app/unit-tests/src/org/commcare/views/connect/ConnectProgressCardTest.kt +++ b/app/unit-tests/src/org/commcare/views/connect/ConnectProgressCardTest.kt @@ -105,6 +105,24 @@ class ConnectProgressCardTest { assertEquals(View.GONE, count.visibility) } + @Test + fun `a null max shows the bare count and hides the bar until a max returns`() { + val card = newCard() + val count = card.findViewById(R.id.progress_card_bar_count) + val bar = card.findViewById(R.id.progress_card_linear_bar) + + card.bind(State(linearProgress = State.LinearProgress(current = 3, max = null))) + + assertEquals(View.VISIBLE, count.visibility) + assertEquals("3", count.text.toString()) + assertEquals(View.GONE, bar.visibility) + + card.bind(State(linearProgress = State.LinearProgress(current = 3, max = 5))) + + assertEquals("3 of 5", count.text.toString()) + assertEquals(View.VISIBLE, bar.visibility) + } + @Test fun `linear current is clamped to max in the count label`() { val card = newCard()