diff --git a/.gitmodules b/.gitmodules
index b908ca7..13ce56c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,4 @@
[submodule "floret-kit"]
path = floret-kit
url = https://codeberg.org/jlmakiola/floret-kit.git
+ branch = feat/clockula
diff --git a/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/AlarmsInstrumentedTest.kt b/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/AlarmsInstrumentedTest.kt
index fa430b9..c882f88 100644
--- a/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/AlarmsInstrumentedTest.kt
+++ b/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/AlarmsInstrumentedTest.kt
@@ -122,7 +122,7 @@ class AlarmsInstrumentedTest {
fun theFabCreatesAnAlarmAndOpensItsEditor() {
openEditorForNewAlarm()
- composeTestRule.onNodeWithText(string(R.string.editor_title)).assertIsDisplayed()
+ composeTestRule.onNodeWithContentDescription("hour", substring = true).assertIsDisplayed()
assertThat(runBlocking { seams.alarms().alarms().first() }).hasSize(1)
}
@@ -205,7 +205,7 @@ class AlarmsInstrumentedTest {
composeTestRule.onNodeWithText("Work").performClick()
composeTestRule.waitForIdle()
- composeTestRule.onNodeWithText(string(R.string.editor_delete)).performClick()
+ composeTestRule.onNodeWithContentDescription(string(R.string.editor_delete)).performClick()
composeTestRule.waitForIdle()
composeTestRule.onNodeWithText(string(R.string.action_delete)).performClick()
composeTestRule.waitForIdle()
diff --git a/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/StopwatchInstrumentedTest.kt b/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/StopwatchInstrumentedTest.kt
index 18f8839..a8c21a5 100644
--- a/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/StopwatchInstrumentedTest.kt
+++ b/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/StopwatchInstrumentedTest.kt
@@ -105,14 +105,14 @@ class StopwatchInstrumentedTest {
/** §5.14 #2 */
@Test
- fun twoTapsOnLapAppendTwoRowsAndLeaveAThirdInProgress() {
+ fun newLapsStayVisibleAtTheTopOfTheList() {
openStopwatchTab()
- startAndLap(times = 2)
+ startAndLap(times = 4)
- assertThat(storedLaps().map { it.index }).containsExactly(1, 2).inOrder()
+ assertThat(storedLaps().map { it.index }).containsExactly(1, 2, 3, 4).inOrder()
composeTestRule
- .onNodeWithContentDescription(context.getString(R.string.stopwatch_lap_number, 3), substring = true)
+ .onNodeWithContentDescription(context.getString(R.string.stopwatch_lap_number, 5), substring = true)
.assertIsDisplayed()
}
diff --git a/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/TimersInstrumentedTest.kt b/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/TimersInstrumentedTest.kt
index 9490f99..745d317 100644
--- a/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/TimersInstrumentedTest.kt
+++ b/app/src/androidTest/java/de/jeanlucmakiola/clockula/ui/TimersInstrumentedTest.kt
@@ -5,6 +5,7 @@ import android.content.Intent
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createEmptyComposeRule
import androidx.compose.ui.test.onAllNodesWithText
+import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.lifecycle.Lifecycle
@@ -96,10 +97,13 @@ class TimersInstrumentedTest {
/** §5.19 #1 */
@Test
fun typingOnTheKeypadAndPressingStartCreatesOneRunningTimer() {
+ // The list starts empty (teardown clears it), so the keypad is
+ // already inline — no FAB tap needed to reach it.
openTimersTab()
typeDigits('1', '3', '0')
- composeTestRule.onNodeWithText(string(R.string.timers_setup_start)).performClick()
+ // The play button is icon-only; "Start" is its content description.
+ composeTestRule.onNodeWithContentDescription(string(R.string.timers_setup_start)).performClick()
composeTestRule.waitForIdle()
val timers = stored()
@@ -116,7 +120,9 @@ class TimersInstrumentedTest {
launched.recreate()
composeTestRule.waitForIdle()
- composeTestRule.onNodeWithText("0:01:30").assertIsDisplayed()
+ // The readout is now three segments ("00h 01m 30s"); its merged
+ // semantics still carry the spoken "0:01:30" form (`TimerDurationReadout`).
+ composeTestRule.onNodeWithContentDescription("0:01:30").assertIsDisplayed()
}
/** §5.19 #3 */
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7a5cc62..72e9b8a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,6 +27,8 @@
+
+
- Crash report
https://codeberg.org/jlmakiola/clockula/issues/new
@@ -35,12 +34,20 @@
Remove preset
Delete a digit
Type %1$s
+ Type 00
+ Clear
+ h
+ m
+ s
Start
Pause
Resume
Reset
Stop
+1 min
+ −1 min
+ +%1$d min
+ −%1$d min
Finished
Paused \u00b7 %1$s
Timer
@@ -48,7 +55,7 @@
Timer settings
- Timer
+ Countdown
Label
Length
Reset the timer to change its length
diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/data/timers/TimerRepositoryTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/data/timers/TimerRepositoryTest.kt
index 4057e50..ca54416 100644
--- a/app/src/test/java/de/jeanlucmakiola/clockula/data/timers/TimerRepositoryTest.kt
+++ b/app/src/test/java/de/jeanlucmakiola/clockula/data/timers/TimerRepositoryTest.kt
@@ -166,6 +166,44 @@ class TimerRepositoryTest {
assertThat(timer.snapshotAt(1_100.seconds, T0).remaining).isEqualTo(260.seconds)
}
+ @Test
+ fun `subtracting a minute rebases a running timer and preserves its configured duration`() = runTest {
+ val id = startedTimer()
+ elapsed.value = 1_100.seconds
+
+ repository.addTime(id, -1.minutes)
+
+ val timer = repository.find(id)!!
+ assertThat(timer.state).isEqualTo(TimerState.RUNNING)
+ assertThat(timer.snapshotAt(1_100.seconds, T0).remaining).isEqualTo(140.seconds)
+ assertThat(timer.endsAtElapsedRealtime).isEqualTo(1_240.seconds)
+ assertThat(timer.duration).isEqualTo(5.minutes)
+ }
+
+ @Test
+ fun `subtracting at or below a minute leaves the timer unchanged`() = runTest {
+ val id = startedTimer()
+ elapsed.value = 1_250.seconds
+ val before = dao.stored.single()
+
+ repository.addTime(id, -1.minutes)
+
+ assertThat(dao.stored.single()).isEqualTo(before)
+ }
+
+ @Test
+ fun `subtracting from paused timer keeps it paused`() = runTest {
+ val id = startedTimer()
+ elapsed.value = 1_100.seconds
+ repository.pause(id)
+
+ repository.addTime(id, -1.minutes)
+
+ val timer = repository.find(id)!!
+ assertThat(timer.state).isEqualTo(TimerState.PAUSED)
+ assertThat(timer.remaining).isEqualTo(140.seconds)
+ }
+
@Test
fun `adding time to a running timer whose end anchor has passed grants the whole extra`() = runTest {
val id = startedTimer()
diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/domain/timer/TimerDurationEntryTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/domain/timer/TimerDurationEntryTest.kt
index f8a4d2d..e04f632 100644
--- a/app/src/test/java/de/jeanlucmakiola/clockula/domain/timer/TimerDurationEntryTest.kt
+++ b/app/src/test/java/de/jeanlucmakiola/clockula/domain/timer/TimerDurationEntryTest.kt
@@ -137,4 +137,23 @@ class TimerDurationEntryTest {
Locale.setDefault(previous)
}
}
+
+ @Test
+ fun `the per-unit segments keep their own leading zero unlike display`() {
+ val entry = typed("130")
+
+ assertThat(listOf(entry.hoursSegment, entry.minutesSegment, entry.secondsSegment))
+ .containsExactly("00", "01", "30")
+ .inOrder()
+ }
+
+ @Test
+ fun `the double-zero key shifts in two zeroes at once`() {
+ assertThat(typed("5").plusDoubleZero()).isEqualTo(typed("500"))
+ }
+
+ @Test
+ fun `a double zero into an empty entry is ignored, same as a single leading zero`() {
+ assertThat(TimerDurationEntry().plusDoubleZero()).isEqualTo(TimerDurationEntry())
+ }
}
diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/domain/timer/TimerNotificationPolicyTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/domain/timer/TimerNotificationPolicyTest.kt
index f9609e4..920c72d 100644
--- a/app/src/test/java/de/jeanlucmakiola/clockula/domain/timer/TimerNotificationPolicyTest.kt
+++ b/app/src/test/java/de/jeanlucmakiola/clockula/domain/timer/TimerNotificationPolicyTest.kt
@@ -58,6 +58,7 @@ class TimerNotificationPolicyTest {
listOf(TimerNotificationAction.PAUSE, TimerNotificationAction.ADD_MINUTE) to
subject.timer.endsAtWallClock,
)
+ assertThat(state.duration).isEqualTo(subject.timer.duration)
}
/** §5.5 #3 */
diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmRoutesTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmRoutesTest.kt
index 989b2ce..ca519fa 100644
--- a/app/src/test/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmRoutesTest.kt
+++ b/app/src/test/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmRoutesTest.kt
@@ -14,6 +14,7 @@ class AlarmRoutesTest {
@Test
fun `the editor route carries the alarm's id`() {
assertThat(AlarmRoutes.editor(7L)).isEqualTo("alarms/edit/7")
+ assertThat(AlarmRoutes.newEditor(7L)).isEqualTo("alarms/edit/7?pickTime=true")
}
@Test
diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/ui/shell/ShellNavigationTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/ui/shell/ShellNavigationTest.kt
index 2f4ebd0..d992d0b 100644
--- a/app/src/test/java/de/jeanlucmakiola/clockula/ui/shell/ShellNavigationTest.kt
+++ b/app/src/test/java/de/jeanlucmakiola/clockula/ui/shell/ShellNavigationTest.kt
@@ -3,6 +3,7 @@ package de.jeanlucmakiola.clockula.ui.shell
import com.google.common.truth.Truth.assertThat
import de.jeanlucmakiola.clockula.stopwatch.StopwatchIntents
import de.jeanlucmakiola.clockula.timer.TimerIntents
+import de.jeanlucmakiola.clockula.ui.alarms.AlarmRoutes
import de.jeanlucmakiola.clockula.ui.timers.TimerRoutes
import org.junit.jupiter.api.Test
@@ -150,6 +151,14 @@ class ShellNavigationTest {
assertThat(ShellNavigation.destinationOf(TimerRoutes.editor(7L))).isNull()
}
+ @Test
+ fun `editors highlight their parent tab`() {
+ assertThat(ShellNavigation.selectedDestinationOf(AlarmRoutes.EDITOR_PATTERN))
+ .isEqualTo(ClockulaDestination.ALARMS)
+ assertThat(ShellNavigation.selectedDestinationOf(TimerRoutes.EDITOR_PATTERN))
+ .isEqualTo(ClockulaDestination.TIMERS)
+ }
+
/** §5.15 #2 */
@Test
fun `the shell stands aside on back, leaving the timer editor to pop itself`() {
diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/ui/timers/TimerListRowsTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/ui/timers/TimerListRowsTest.kt
index 0b606f5..c116cec 100644
--- a/app/src/test/java/de/jeanlucmakiola/clockula/ui/timers/TimerListRowsTest.kt
+++ b/app/src/test/java/de/jeanlucmakiola/clockula/ui/timers/TimerListRowsTest.kt
@@ -68,10 +68,10 @@ class TimerListRowsTest {
/** §5.11 #4 */
@Test
- fun `an idle timer reads its configured length, full`() {
+ fun `an idle timer reads its configured length, empty progress`() {
val row = rows(listOf(idleTimer(duration = 5.minutes))).single()
- assertThat(row.remaining to row.progress).isEqualTo(5.minutes to 1f)
+ assertThat(row.remaining to row.progress).isEqualTo(5.minutes to 0f)
}
/** §5.11 #5 */
@@ -89,15 +89,15 @@ class TimerListRowsTest {
val row = rows(listOf(timer)).single()
- assertThat(row.remaining to row.progress).isEqualTo(60.seconds to 0.2f)
+ assertThat(row.remaining to row.progress).isEqualTo(60.seconds to 0.8f)
}
/** §5.11 #7 */
@Test
- fun `an expired timer reads empty`() {
+ fun `an expired timer reads full progress`() {
val row = rows(listOf(expiredTimer(duration = 5.minutes))).single()
- assertThat(row.remaining to row.progress).isEqualTo(Duration.ZERO to 0f)
+ assertThat(row.remaining to row.progress).isEqualTo(Duration.ZERO to 1f)
}
/** §5.11 #8 */
@@ -110,10 +110,10 @@ class TimerListRowsTest {
/** §5.11 #9 */
@Test
- fun `a timer with more left than its length is clamped full`() {
+ fun `a timer with more left than its length is clamped empty`() {
val timer = runningTimerWith(left = 10.minutes, elapsedNow = now, duration = 5.minutes)
- assertThat(rows(listOf(timer)).single().progress).isEqualTo(1f)
+ assertThat(rows(listOf(timer)).single().progress).isEqualTo(0f)
}
/** §5.11 #10 */
diff --git a/docs/issues.md b/docs/issues.md
new file mode 100644
index 0000000..e69de29
diff --git a/floret-kit b/floret-kit
index 1934306..18a7977 160000
--- a/floret-kit
+++ b/floret-kit
@@ -1 +1 @@
-Subproject commit 1934306903ec38df81351ef13a17d5f2ab14fd43
+Subproject commit 18a797747b6f27032bf674415af857a1223b6975