P3 Stream K: Glance widgets (small/medium/large) matching iOS
- HoneyDueSmallWidget (2x2), HoneyDueMediumWidget (4x2), HoneyDueLargeWidget (4x4) rewritten to consume the iOS-parity WidgetDataRepository (Stream J). - Free-tier shows a large count-only layout (matches iOS FreeWidgetView); premium shows task list + complete buttons (Large widget also renders the Overdue / 7 Days / 30 Days stats row from WidgetDataRepository.computeStats()). - WidgetFormatter mirrors iOS formatWidgetDate: "Today" / "in N day(s)" / "N day(s) ago". - WidgetColors maps priority levels (1-4) to primary/yellow/ accent/error, matching iOS OrganicTaskRowView.priorityColor. - WidgetUi shared Glance composables (TaskRow, WidgetHeader, EmptyState, TaskCountBlock, StatPill, StatsRow, CompleteButton) wired to Stream M's CompleteTaskAction for interactive rows. - JVM tests: WidgetFormatterTest + WidgetColorsTest covering 10 formatter assertions and 11 color mapping assertions. Glance caveats: no radial/linear gradients or custom shapes, so iOS's "organic" glows are dropped in favor of cream background + tinted TaskRow cards. Colors, typography, and priority semantics match iOS exactly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package com.tt.honeyDue.widget
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Unit test for [WidgetColors].
|
||||
*
|
||||
* Priority color mapping mirrors iOS `OrganicTaskRowView.priorityColor`
|
||||
* in `iosApp/HoneyDue/HoneyDue.swift`:
|
||||
*
|
||||
* - urgent → appError
|
||||
* - high → appAccent
|
||||
* - medium → yellow
|
||||
* - low → appPrimary
|
||||
* - overdue → appError (overrides everything else)
|
||||
*
|
||||
* Priority "level" values match the backend seed in
|
||||
* `MyCribAPI_GO/internal/testutil/testutil.go`:
|
||||
* 1 = Low, 2 = Medium, 3 = High, 4 = Urgent.
|
||||
*/
|
||||
class WidgetColorsTest {
|
||||
|
||||
@Test
|
||||
fun colorForPriority_urgent_is_error() {
|
||||
assertEquals(WidgetColors.ERROR, WidgetColors.colorForPriority(priorityLevel = 4))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun colorForPriority_high_is_accent() {
|
||||
assertEquals(WidgetColors.ACCENT, WidgetColors.colorForPriority(priorityLevel = 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun colorForPriority_medium_is_yellow() {
|
||||
assertEquals(WidgetColors.YELLOW_MEDIUM, WidgetColors.colorForPriority(priorityLevel = 2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun colorForPriority_low_is_primary() {
|
||||
assertEquals(WidgetColors.PRIMARY, WidgetColors.colorForPriority(priorityLevel = 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun colorForPriority_unknown_defaults_to_primary() {
|
||||
// iOS default branch falls through to appPrimary for any non-urgent/high/medium.
|
||||
assertEquals(WidgetColors.PRIMARY, WidgetColors.colorForPriority(priorityLevel = 0))
|
||||
assertEquals(WidgetColors.PRIMARY, WidgetColors.colorForPriority(priorityLevel = 99))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun colorForOverdue_true_returns_error() {
|
||||
assertEquals(WidgetColors.ERROR, WidgetColors.colorForOverdue(isOverdue = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun colorForOverdue_false_returns_textSecondary() {
|
||||
// iOS "Overdue" pill uses appTextSecondary when there's nothing overdue.
|
||||
assertEquals(WidgetColors.TEXT_SECONDARY, WidgetColors.colorForOverdue(isOverdue = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun taskRowColor_overdue_beats_priority() {
|
||||
// iOS OrganicTaskRowView: `if task.isOverdue { return .appError }` first.
|
||||
val c = WidgetColors.taskRowColor(priorityLevel = 1, isOverdue = true)
|
||||
assertEquals(WidgetColors.ERROR, c)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun taskRowColor_not_overdue_uses_priority() {
|
||||
assertEquals(
|
||||
WidgetColors.ACCENT,
|
||||
WidgetColors.taskRowColor(priorityLevel = 3, isOverdue = false)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dueDateTextColor_overdue_is_error_otherwise_accent() {
|
||||
// iOS: `.foregroundStyle(task.isOverdue ? Color.appError : Color.appAccent)`
|
||||
assertEquals(WidgetColors.ERROR, WidgetColors.dueDateTextColor(isOverdue = true))
|
||||
assertEquals(WidgetColors.ACCENT, WidgetColors.dueDateTextColor(isOverdue = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun colors_are_stable_instances() {
|
||||
// Sanity: make sure the constants aren't null/default — helps catch
|
||||
// a refactor that accidentally resets them to Color.Unspecified.
|
||||
assertEquals(Color(0xFF07A0C3), WidgetColors.PRIMARY)
|
||||
assertEquals(Color(0xFFF5A623), WidgetColors.ACCENT)
|
||||
assertEquals(Color(0xFFDD1C1A), WidgetColors.ERROR)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.tt.honeyDue.widget
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Pure-JVM unit test for [WidgetFormatter.formatDueDateRelative].
|
||||
*
|
||||
* Mirrors the iOS `formatWidgetDate(_:)` helper in
|
||||
* `iosApp/HoneyDue/HoneyDue.swift` — both are responsible for rendering
|
||||
* a short human-friendly string from a `daysUntilDue` offset. Exact
|
||||
* wording must match so the two platforms ship indistinguishable widgets.
|
||||
*/
|
||||
class WidgetFormatterTest {
|
||||
|
||||
@Test
|
||||
fun formatDueDateRelative_today() {
|
||||
assertEquals("Today", WidgetFormatter.formatDueDateRelative(daysUntilDue = 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatDueDateRelative_tomorrow_is_in_1_day() {
|
||||
// iOS formatter returns "in 1 day" for days==1 (it has no special
|
||||
// "Tomorrow" case — only "Today" and then "in N day(s)" / "N day(s) ago").
|
||||
assertEquals("in 1 day", WidgetFormatter.formatDueDateRelative(daysUntilDue = 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatDueDateRelative_in_3_days() {
|
||||
assertEquals("in 3 days", WidgetFormatter.formatDueDateRelative(daysUntilDue = 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatDueDateRelative_in_7_days() {
|
||||
assertEquals("in 7 days", WidgetFormatter.formatDueDateRelative(daysUntilDue = 7))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatDueDateRelative_one_day_ago() {
|
||||
assertEquals("1 day ago", WidgetFormatter.formatDueDateRelative(daysUntilDue = -1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatDueDateRelative_five_days_ago() {
|
||||
assertEquals("5 days ago", WidgetFormatter.formatDueDateRelative(daysUntilDue = -5))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun taskCountLabel_singular_plural() {
|
||||
// iOS FreeWidgetView: "task waiting" / "tasks waiting"
|
||||
assertEquals("task waiting", WidgetFormatter.taskCountLabel(1))
|
||||
assertEquals("tasks waiting", WidgetFormatter.taskCountLabel(0))
|
||||
assertEquals("tasks waiting", WidgetFormatter.taskCountLabel(5))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compactTaskCountLabel_singular_plural() {
|
||||
// iOS Small/Medium widgets: short "task"/"tasks" under the count.
|
||||
assertEquals("task", WidgetFormatter.compactTaskCountLabel(1))
|
||||
assertEquals("tasks", WidgetFormatter.compactTaskCountLabel(0))
|
||||
assertEquals("tasks", WidgetFormatter.compactTaskCountLabel(3))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user