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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
|
;;; test-chime-group-events-by-day.el --- Tests for chime--group-events-by-day -*- lexical-binding: t; -*-
;; Copyright (C) 2024-2026 Craig Jennings
;; Author: Craig Jennings <c@cjennings.net>
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Unit tests for chime--group-events-by-day function.
;; Tests cover normal cases, boundary cases, and error cases.
;;
;; Note: chime--group-events-by-day does not handle malformed events gracefully.
;; This is acceptable since events are generated internally by chime and should
;; always have the correct structure. If a malformed event is passed, it will error.
;; Removed test: test-chime-group-events-by-day-error-malformed-event
;;
;; Tests that build events as a fixed-minute offset from "now" pin the clock
;; with `with-test-time' so the offsets do not accidentally cross a calendar
;; day boundary on late-night runs. The clock value is bound in a `let' first
;; (the macro re-evaluates BASE-TIME on every `current-time' call, so passing
;; `(test-time-today-at ...)' directly would recurse — `test-time-today-at'
;; calls `current-time' itself).
;;; Code:
(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
;; Load test utilities
(require 'testutil-general (expand-file-name "testutil-general.el"))
(require 'testutil-time (expand-file-name "testutil-time.el"))
;;; Setup and Teardown
(defun test-chime-group-events-by-day-setup ()
"Setup function run before each test."
(chime-create-test-base-dir))
(defun test-chime-group-events-by-day-teardown ()
"Teardown function run after each test."
(chime-delete-test-base-dir))
;;; Test Helpers
(defun test-chime-make-event-item (minutes-until title)
"Create a mock event item for testing.
MINUTES-UNTIL is minutes until event, TITLE is event title."
(let* ((now (current-time))
(event-time (time-add now (seconds-to-time (* minutes-until 60))))
(timestamp-str (test-timestamp-string event-time))
(event `((title . ,title)
(times . ())))
(time-info (cons timestamp-str event-time)))
(list event time-info minutes-until)))
;;; Normal Cases
(ert-deftest test-chime-group-events-by-day-normal-single-day ()
"Test grouping events all on same day."
(test-chime-group-events-by-day-setup)
(unwind-protect
(let ((pinned (test-time-today-at 12 0)))
(with-test-time pinned
(let* ((event1 (test-chime-make-event-item 10 "Event 1"))
(event2 (test-chime-make-event-item 30 "Event 2"))
(event3 (test-chime-make-event-item 60 "Event 3"))
(upcoming (list event1 event2 event3))
(result (chime--group-events-by-day upcoming)))
;; Should have 1 group (today)
(should (= 1 (length result)))
;; Group should have 3 events
(should (= 3 (length (cdr (car result)))))
;; Date string should say "Today"
(should (string-match-p "Today" (car (car result)))))))
(test-chime-group-events-by-day-teardown)))
(ert-deftest test-chime-group-events-by-day-normal-multiple-days ()
"Test grouping events across multiple days."
(test-chime-group-events-by-day-setup)
(unwind-protect
(let ((pinned (test-time-today-at 12 0)))
(with-test-time pinned
(let* ((event1 (test-chime-make-event-item 10 "Today Event"))
(event2 (test-chime-make-event-item 1500 "Tomorrow Event")) ; > 1440
(event3 (test-chime-make-event-item 3000 "Future Event")) ; > 2880
(upcoming (list event1 event2 event3))
(result (chime--group-events-by-day upcoming)))
;; Should have 3 groups (today, tomorrow, future)
(should (= 3 (length result)))
;; First group should say "Today"
(should (string-match-p "Today" (car (nth 0 result))))
;; Second group should say "Tomorrow"
(should (string-match-p "Tomorrow" (car (nth 1 result)))))))
(test-chime-group-events-by-day-teardown)))
(ert-deftest test-chime-group-events-by-day-normal-maintains-order ()
"Test that events maintain order within groups."
(test-chime-group-events-by-day-setup)
(unwind-protect
(let ((pinned (test-time-today-at 12 0)))
(with-test-time pinned
(let* ((event1 (test-chime-make-event-item 10 "First"))
(event2 (test-chime-make-event-item 20 "Second"))
(event3 (test-chime-make-event-item 30 "Third"))
(upcoming (list event1 event2 event3))
(result (chime--group-events-by-day upcoming))
(today-events (cdr (car result))))
;; Should maintain order
(should (= 3 (length today-events)))
(should (string= "First" (cdr (assoc 'title (car (nth 0 today-events))))))
(should (string= "Second" (cdr (assoc 'title (car (nth 1 today-events))))))
(should (string= "Third" (cdr (assoc 'title (car (nth 2 today-events)))))))))
(test-chime-group-events-by-day-teardown)))
;;; Boundary Cases
(ert-deftest test-chime-group-events-by-day-boundary-empty-list ()
"Test grouping empty events list."
(test-chime-group-events-by-day-setup)
(unwind-protect
(let ((result (chime--group-events-by-day '())))
;; Should return empty list
(should (null result)))
(test-chime-group-events-by-day-teardown)))
(ert-deftest test-chime-group-events-by-day-boundary-single-event ()
"Test grouping single event."
(test-chime-group-events-by-day-setup)
(unwind-protect
(let ((pinned (test-time-today-at 12 0)))
(with-test-time pinned
(let* ((event (test-chime-make-event-item 10 "Only Event"))
(upcoming (list event))
(result (chime--group-events-by-day upcoming)))
;; Should have 1 group
(should (= 1 (length result)))
;; Group should have 1 event
(should (= 1 (length (cdr (car result))))))))
(test-chime-group-events-by-day-teardown)))
(ert-deftest test-chime-group-events-by-day-boundary-exactly-1440-minutes ()
"Test event at exactly 1440 minutes (1 day boundary)."
(test-chime-group-events-by-day-setup)
(unwind-protect
(let ((pinned (test-time-today-at 12 0)))
(with-test-time pinned
(let* ((event (test-chime-make-event-item 1440 "Boundary Event"))
(upcoming (list event))
(result (chime--group-events-by-day upcoming)))
;; Should be grouped as "Tomorrow"
(should (= 1 (length result)))
(should (string-match-p "Tomorrow" (car (car result)))))))
(test-chime-group-events-by-day-teardown)))
(ert-deftest test-chime-group-events-by-day-boundary-just-under-1440 ()
"Test event at 1439 minutes (23h 59m away).
If current time is noon, an event 1439 minutes away is at 11:59 AM the
next calendar day, so it should be grouped as 'Tomorrow', not 'Today'."
(test-chime-group-events-by-day-setup)
(unwind-protect
(let ((pinned (test-time-today-at 12 0)))
(with-test-time pinned
(let* ((event (test-chime-make-event-item 1439 "Almost Tomorrow"))
(upcoming (list event))
(result (chime--group-events-by-day upcoming)))
;; Should be grouped as "Tomorrow" (next calendar day)
(should (= 1 (length result)))
(should (string-match-p "Tomorrow" (car (car result)))))))
(test-chime-group-events-by-day-teardown)))
(ert-deftest test-chime-group-events-by-day-boundary-exactly-2880-minutes ()
"Test event at exactly 2880 minutes (2 day boundary)."
(test-chime-group-events-by-day-setup)
(unwind-protect
(let ((pinned (test-time-today-at 12 0)))
(with-test-time pinned
(let* ((event (test-chime-make-event-item 2880 "Two Days Away"))
(upcoming (list event))
(result (chime--group-events-by-day upcoming)))
;; Should be grouped as a future day (not "Tomorrow")
(should (= 1 (length result)))
(should-not (string-match-p "Tomorrow" (car (car result)))))))
(test-chime-group-events-by-day-teardown)))
(ert-deftest test-chime-group-events-by-day-boundary-zero-minutes ()
"Test event at 0 minutes (happening now)."
(test-chime-group-events-by-day-setup)
(unwind-protect
(let ((pinned (test-time-today-at 12 0)))
(with-test-time pinned
(let* ((event (test-chime-make-event-item 0 "Right Now"))
(upcoming (list event))
(result (chime--group-events-by-day upcoming)))
;; Should be grouped as "Today"
(should (= 1 (length result)))
(should (string-match-p "Today" (car (car result)))))))
(test-chime-group-events-by-day-teardown)))
(ert-deftest test-chime-group-events-by-day-boundary-event-just-before-midnight-today ()
"Boundary: clock at 23:50, event +5 min lands at 23:55 (still today)."
(test-chime-group-events-by-day-setup)
(unwind-protect
(let ((pinned (test-time-today-at 23 50)))
(with-test-time pinned
(let* ((event (test-chime-make-event-item 5 "Late Tonight"))
(upcoming (list event))
(result (chime--group-events-by-day upcoming)))
(should (= 1 (length result)))
(should (string-match-p "Today" (car (car result)))))))
(test-chime-group-events-by-day-teardown)))
(ert-deftest test-chime-group-events-by-day-boundary-event-crossing-midnight-tomorrow ()
"Boundary: clock at 23:50, event +20 min lands at 00:10 next day (Tomorrow)."
(test-chime-group-events-by-day-setup)
(unwind-protect
(let ((pinned (test-time-today-at 23 50)))
(with-test-time pinned
(let* ((event (test-chime-make-event-item 20 "Across Midnight"))
(upcoming (list event))
(result (chime--group-events-by-day upcoming)))
(should (= 1 (length result)))
(should (string-match-p "Tomorrow" (car (car result)))))))
(test-chime-group-events-by-day-teardown)))
;;; Bug Reproduction Tests
(ert-deftest test-chime-group-events-by-day-bug-tomorrow-morning-grouped-as-today ()
"Test that tomorrow morning event is NOT grouped as 'Today'.
This reproduces the bug where an event at 10:00 AM tomorrow,
when it's 11:23 AM today (22h 37m = 1357 minutes away),
is incorrectly grouped as 'Today' instead of 'Tomorrow'.
The bug: The function groups by 24-hour period (<1440 minutes)
instead of by calendar day."
(test-chime-group-events-by-day-setup)
(unwind-protect
(with-test-time (encode-time 0 23 11 2 11 2025) ; Nov 02, 2025 11:23 AM
(let* ((now (current-time))
(tomorrow-morning (encode-time 0 0 10 3 11 2025)) ; Nov 03, 2025 10:00 AM
(minutes-until (/ (- (float-time tomorrow-morning) (float-time now)) 60))
;; Create event manually since test-chime-make-event-item uses relative time
(event `((title . "Transit to Meeting")
(times . ())))
(time-info (cons (test-timestamp-string tomorrow-morning) tomorrow-morning))
(event-item (list event time-info minutes-until))
(upcoming (list event-item))
(result (chime--group-events-by-day upcoming)))
;; Verify it's less than 1440 minutes (this is why the bug happens)
(should (< minutes-until 1440))
;; Should have 1 group
(should (= 1 (length result)))
;; BUG: Currently groups as "Today" but should be "Tomorrow"
;; because the event is on Nov 03, not Nov 02
(should (string-match-p "Tomorrow" (car (car result))))))
(test-chime-group-events-by-day-teardown)))
;;;; Tests for chime--day-label-for-event-time
;; Pure-function tests. NOW and TOMORROW are passed in, so no clock mocking
;; is needed — we just construct stable calendar times with `encode-time'.
(ert-deftest test-chime-day-label-normal-same-day-returns-today ()
"Normal: event on the same calendar day as NOW returns \"Today, ...\"."
(let* ((now (encode-time 0 0 12 15 5 2026)) ; 2026-05-15 12:00
(tomorrow (time-add now (days-to-time 1)))
(event (encode-time 0 0 9 15 5 2026)) ; 2026-05-15 09:00
(label (chime--day-label-for-event-time event now tomorrow)))
(should (stringp label))
(should (string-match-p "\\`Today" label))
(should (string-match-p "May 15" label))))
(ert-deftest test-chime-day-label-normal-next-calendar-day-returns-tomorrow ()
"Normal: event on the same calendar day as TOMORROW returns \"Tomorrow, ...\"."
(let* ((now (encode-time 0 0 12 15 5 2026)) ; 2026-05-15 12:00
(tomorrow (time-add now (days-to-time 1)))
(event (encode-time 0 0 10 16 5 2026)) ; 2026-05-16 10:00
(label (chime--day-label-for-event-time event now tomorrow)))
(should (string-match-p "\\`Tomorrow" label))
(should (string-match-p "May 16" label))))
(ert-deftest test-chime-day-label-normal-distant-future-returns-weekday ()
"Normal: event three days out returns a weekday label (not Today/Tomorrow)."
(let* ((now (encode-time 0 0 12 15 5 2026)) ; 2026-05-15 (Fri) 12:00
(tomorrow (time-add now (days-to-time 1)))
(event (encode-time 0 0 14 18 5 2026)) ; 2026-05-18 (Mon) 14:00
(label (chime--day-label-for-event-time event now tomorrow)))
(should (stringp label))
(should-not (string-match-p "\\`Today" label))
(should-not (string-match-p "\\`Tomorrow" label))
(should (string-match-p "Monday" label))
(should (string-match-p "May 18" label))))
(ert-deftest test-chime-day-label-boundary-event-at-today-midnight ()
"Boundary: event at 00:00 on the same calendar day as NOW is Today."
(let* ((now (encode-time 0 0 12 15 5 2026)) ; 2026-05-15 12:00
(tomorrow (time-add now (days-to-time 1)))
(event (encode-time 0 0 0 15 5 2026)) ; 2026-05-15 00:00
(label (chime--day-label-for-event-time event now tomorrow)))
(should (string-match-p "\\`Today" label))))
(ert-deftest test-chime-day-label-boundary-event-at-tomorrow-midnight ()
"Boundary: event at 00:00 on TOMORROW's calendar day is Tomorrow."
(let* ((now (encode-time 0 0 12 15 5 2026)) ; 2026-05-15 12:00
(tomorrow (time-add now (days-to-time 1)))
(event (encode-time 0 0 0 16 5 2026)) ; 2026-05-16 00:00
(label (chime--day-label-for-event-time event now tomorrow)))
(should (string-match-p "\\`Tomorrow" label))))
(ert-deftest test-chime-day-label-boundary-day-after-tomorrow-is-weekday ()
"Boundary: event two calendar days out is NOT Tomorrow; gets a weekday label."
(let* ((now (encode-time 0 0 12 15 5 2026)) ; 2026-05-15 12:00
(tomorrow (time-add now (days-to-time 1)))
(event (encode-time 0 0 12 17 5 2026)) ; 2026-05-17 12:00
(label (chime--day-label-for-event-time event now tomorrow)))
(should-not (string-match-p "\\`Tomorrow" label))
(should (string-match-p "May 17" label))))
(ert-deftest test-chime-day-label-boundary-yesterday-is-weekday ()
"Boundary: event on the previous calendar day is neither Today nor Tomorrow."
(let* ((now (encode-time 0 0 12 15 5 2026)) ; 2026-05-15 12:00
(tomorrow (time-add now (days-to-time 1)))
(event (encode-time 0 0 10 14 5 2026)) ; 2026-05-14 10:00
(label (chime--day-label-for-event-time event now tomorrow)))
(should-not (string-match-p "\\`Today" label))
(should-not (string-match-p "\\`Tomorrow" label))
(should (string-match-p "May 14" label))))
;;; Error Cases
(ert-deftest test-chime-group-events-by-day-error-nil-input ()
"Test that nil input doesn't crash."
(test-chime-group-events-by-day-setup)
(unwind-protect
(progn
;; Should not crash with nil
(should-not (condition-case nil
(progn (chime--group-events-by-day nil) nil)
(error t))))
(test-chime-group-events-by-day-teardown)))
(provide 'test-chime-group-events-by-day)
;;; test-chime-group-events-by-day.el ends here
|