1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
package org.terst.doot.widget.ui
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.After
import org.junit.Test
import org.terst.doot.widget.data.WidgetItem
import java.time.Instant
import java.util.TimeZone
class DootWidgetGridTest {
private lateinit var originalTimeZone: TimeZone
@Before
fun setUp() {
originalTimeZone = TimeZone.getDefault()
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
}
@After
fun tearDown() {
TimeZone.setDefault(originalTimeZone)
}
private fun tomorrowEvent(startIso: String, endIso: String): WidgetItem = WidgetItem(
id = "tmrw", title = "Tomorrow Event", source = "calendar", type = "event",
start = startIso, end = endIso
)
@Test
fun `calcGridStart on an unfiltered mixed list is skewed by tomorrow's event hour`() {
// Documents why WidgetRoot MUST pre-filter to today-only events before calling
// this function: it has no date awareness, only hour-of-day. "now" is 22:00
// today (2026-07-12); the only item is tomorrow's (2026-07-13) 10:00 event.
val mixedList = listOf(tomorrowEvent("2026-07-13T10:00:00Z", "2026-07-13T14:30:00Z"))
assertEquals(9, calcGridStart(mixedList, nowHour = 22))
}
@Test
fun `calcGridStart on the correctly today-filtered (empty) list falls back to nowHour`() {
val gridStart = calcGridStart(emptyList(), nowHour = 22)
assertEquals(21, gridStart)
}
@Test
fun `calcGridEnd falls back to nowHour plus eight when no today events, capped at 23`() {
val gridEnd = calcGridEnd(emptyList(), nowHour = 22)
assertEquals(23, gridEnd)
}
// isPastEvent: 2026-08-05 change pulling already-ended events out of the grid to
// stack above it as list rows, same treatment past tasks already got.
private fun event(endIso: String?) = WidgetItem(
id = "e", title = "Event", source = "calendar", type = "event",
start = "2026-07-12T09:00:00Z", end = endIso
)
private fun task(startIso: String) = WidgetItem(
id = "t", title = "Task", source = "doot", type = "task", start = startIso
)
@Test
fun `isPastEvent is true once end has passed`() {
val now = Instant.parse("2026-07-12T10:00:00Z")
assertTrue(isPastEvent(event(endIso = "2026-07-12T09:30:00Z"), now))
}
@Test
fun `isPastEvent is false while still ongoing`() {
val now = Instant.parse("2026-07-12T10:00:00Z")
assertFalse(isPastEvent(event(endIso = "2026-07-12T10:30:00Z"), now))
}
@Test
fun `isPastEvent is false with no end time -- never considered past`() {
val now = Instant.parse("2026-07-12T23:00:00Z")
assertFalse(isPastEvent(event(endIso = null), now))
}
@Test
fun `isPastEvent is always false for tasks -- SlotPacker already handles those`() {
val now = Instant.parse("2026-07-12T10:00:00Z")
assertFalse(isPastEvent(task(startIso = "2026-07-12T08:00:00Z"), now))
}
// moonPhaseEmoji: values verified against direct computation before writing these
// assertions (days-since-anchor / synodic-month arithmetic), not hand-computed.
@Test
fun `moonPhaseEmoji on the reference new moon date is new moon`() {
assertEquals("🌑", moonPhaseEmoji(java.time.LocalDate.of(2000, 1, 6)))
}
@Test
fun `moonPhaseEmoji about half a synodic month later is full moon`() {
assertEquals("🌕", moonPhaseEmoji(java.time.LocalDate.of(2000, 1, 21)))
}
@Test
fun `moonPhaseEmoji never indexes out of bounds across a full cycle`() {
val anchor = java.time.LocalDate.of(2000, 1, 6)
for (offset in 0..365) {
// Must not throw for any date in a full year -- the real assertion here is
// "doesn't crash," the emoji values themselves are checked in the two tests above.
moonPhaseEmoji(anchor.plusDays(offset.toLong()))
}
}
}
|