@@ -133,6 +133,103 @@ class MultiDayEntryE2eTest {
133133 assertEquals(24 * 60 * 60 , persisted.duration)
134134 }
135135
136+ @BackendPortable
137+ @Test
138+ fun shorteningOneAndAHalfDayEntryToOneDayByDurationSyncsWithSolidtime () {
139+ val zone = e2e.session.zone
140+ val startDate = YearMonth .now(zone).atDay(1 )
141+ val start = startDate.atTime(10 , 0 ).atZone(zone).toInstant()
142+ val entry = TimeEntry (
143+ id = ENTRY_ID ,
144+ description = " One-and-a-half-day deployment" ,
145+ userId = e2e.session.userId,
146+ start = start.toString(),
147+ end = start.plusSeconds(36 * 60 * 60L ).toString(),
148+ duration = 36 * 60 * 60 ,
149+ organizationId = e2e.session.organizationId,
150+ )
151+ val handle = e2e.prepare(E2eFixture .Completed (entry))
152+ e2e.launchApp()
153+
154+ TrackRobot (e2e.composeRule)
155+ .waitForHistory()
156+ .assertEntryVisible(entry.description!! )
157+ .tapFirstEntryEdit()
158+ .replaceSheetDuration((24 * 60 ).toString())
159+ .saveSheet()
160+
161+ val snapshot = e2e.awaitServer(WAIT_MS , driveSync = true ) { current ->
162+ current.entry(handle)?.let { persisted ->
163+ persisted.start == start.toString() &&
164+ persisted.end == start.plusSeconds(24 * 60 * 60L ).toString() &&
165+ persisted.duration == 24 * 60 * 60
166+ } == true
167+ }
168+ val persisted = requireNotNull(snapshot.entry(handle))
169+ assertEquals(start.toString(), persisted.start)
170+ assertEquals(start.plusSeconds(24 * 60 * 60L ).toString(), persisted.end)
171+ assertEquals(24 * 60 * 60 , persisted.duration)
172+ }
173+
174+ @Test
175+ fun delayedRefreshCannotRestoreTheOldDurationAfterEditSyncs () {
176+ val mock = e2e.requireMockBackend()
177+ val zone = e2e.session.zone
178+ val startDate = YearMonth .now(zone).atDay(1 )
179+ val start = startDate.atTime(10 , 0 ).atZone(zone).toInstant()
180+ val oldEnd = start.plusSeconds(36 * 60 * 60L )
181+ val newEnd = start.plusSeconds(24 * 60 * 60L )
182+ val entry = TimeEntry (
183+ id = ENTRY_ID ,
184+ description = " Refresh race deployment" ,
185+ userId = e2e.session.userId,
186+ start = start.toString(),
187+ end = oldEnd.toString(),
188+ duration = 36 * 60 * 60 ,
189+ organizationId = e2e.session.organizationId,
190+ )
191+ val handle = e2e.prepare(E2eFixture .Completed (entry))
192+ e2e.launchApp()
193+ val robot = TrackRobot (e2e.composeRule)
194+ .waitForHistory()
195+ .assertEntryVisible(entry.description!! )
196+
197+ val historyCallsBeforeRefresh = mock.callsMatching(" GET" , " /time-entries" ).size
198+ val delayedResponseGate = mock.delayNextTimeEntriesResponseUntilReleased()
199+ try {
200+ robot.tapRefresh()
201+ e2e.composeRule.waitUntil(WAIT_MS ) {
202+ mock.callsMatching(" GET" , " /time-entries" ).size > historyCallsBeforeRefresh
203+ }
204+
205+ robot.tapFirstEntryEdit()
206+ .replaceSheetDuration((24 * 60 ).toString())
207+ .saveSheet()
208+
209+ val snapshot = e2e.awaitServer(WAIT_MS , driveSync = true ) { current ->
210+ current.entry(handle)?.let { persisted ->
211+ persisted.end == newEnd.toString() && persisted.duration == 24 * 60 * 60
212+ } == true
213+ }
214+ assertEquals(newEnd.toString(), snapshot.entry(handle)?.end)
215+ e2e.composeRule.waitUntil(WAIT_MS ) { e2e.pendingOutboxCount() == 0 }
216+
217+ // Release the deliberately stale response only after the server write has completed.
218+ delayedResponseGate.countDown()
219+ e2e.composeRule.waitUntil(WAIT_MS ) {
220+ e2e.localEntry(handle)?.end == newEnd.toString()
221+ }
222+ val newEndTime = newEnd.atZone(zone).format(DateTimeFormatter .ofPattern(" HH:mm" ))
223+ e2e.composeRule.onAllNodes(
224+ hasTestTag(TestTags .trackEntryTimeRange(requireNotNull(handle.serverId))) and
225+ hasText(newEndTime, substring = true ),
226+ useUnmergedTree = true ,
227+ ).onFirst().assertIsDisplayed()
228+ } finally {
229+ delayedResponseGate.countDown()
230+ }
231+ }
232+
136233 private fun assertCalendarCellTotal (day : LocalDate , total : String ) {
137234 // The clickable day cell merges its descendants into one semantics node on real devices.
138235 // Match that stable merged node directly instead of walking an unmerged ancestor chain,
0 commit comments