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
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
|
;;; test-chime-update-modeline.el --- Tests for chime--update-modeline -*- 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--update-modeline function.
;; Tests cover normal cases, boundary cases, and error cases.
;;; 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-update-modeline-setup ()
"Setup function run before each test."
(chime-create-test-base-dir)
;; Reset modeline settings
(setq chime-modeline-string nil)
(setq chime-enable-modeline t)
(setq chime-modeline-lookahead-minutes 30)
(setq chime-modeline-format " ⏰ %s")
;; Disable no-events indicator for tests that expect nil modeline
(setq chime-modeline-no-events-text nil)
(setq chime-tooltip-lookahead-hours nil)) ; Use modeline lookahead
(defun test-chime-update-modeline-teardown ()
"Teardown function run after each test."
(chime-delete-test-base-dir)
(setq chime-modeline-string nil))
;;; Normal Cases
(ert-deftest test-chime-update-modeline-single-event-within-window-updates-modeline ()
"Test that single event within lookahead window updates modeline.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let* ((now (test-time-today-at 14 0))
;; Event at 14:10 (10 minutes from now)
(event-time (test-time-today-at 14 10))
(timestamp-str (test-timestamp-string event-time)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* ((event `((times . ((,timestamp-str . ,event-time)))
(title . "Team Meeting")))
(events (list event)))
(chime--update-modeline events)
;; Should set modeline string
(should chime-modeline-string)
(should (stringp chime-modeline-string))
(should (string-match-p "Team Meeting" chime-modeline-string))
(should (string-match-p "10 minutes" chime-modeline-string))))))
(test-chime-update-modeline-teardown)))
(ert-deftest test-chime-update-modeline-multiple-events-picks-soonest ()
"Test that with multiple events, soonest one is shown.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let* ((now (test-time-today-at 14 0))
;; Event 1 at 14:05 (5 minutes - soonest)
(event-time-1 (test-time-today-at 14 5))
(timestamp-str-1 (test-timestamp-string event-time-1))
;; Event 2 at 14:25 (25 minutes)
(event-time-2 (test-time-today-at 14 25))
(timestamp-str-2 (test-timestamp-string event-time-2)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* ((event1 `((times . ((,timestamp-str-1 . ,event-time-1)))
(title . "Standup")))
(event2 `((times . ((,timestamp-str-2 . ,event-time-2)))
(title . "Code Review")))
(events (list event1 event2)))
(chime--update-modeline events)
;; Should show the soonest event
(should chime-modeline-string)
(should (string-match-p "Standup" chime-modeline-string))
(should-not (string-match-p "Code Review" chime-modeline-string))))))
(test-chime-update-modeline-teardown)))
(ert-deftest test-chime-update-modeline-event-outside-window-no-update ()
"Test that event outside lookahead window doesn't update modeline.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let* ((now (test-time-today-at 14 0))
;; Event at 15:10 (70 minutes from now, outside 30 minute window)
(event-time (test-time-today-at 15 10))
(timestamp-str (test-timestamp-string event-time)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* ((event `((times . ((,timestamp-str . ,event-time)))
(title . "Far Future Event")))
(events (list event)))
(chime--update-modeline events)
;; Should NOT set modeline string
(should-not chime-modeline-string)))))
(test-chime-update-modeline-teardown)))
(ert-deftest test-chime-update-modeline-zero-lookahead-clears-modeline ()
"Test that zero lookahead clears modeline.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let* ((now (test-time-today-at 14 0))
(event-time (test-time-today-at 14 10))
(timestamp-str (test-timestamp-string event-time)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* ((event `((times . ((,timestamp-str . ,event-time)))
(title . "Team Meeting")))
(events (list event)))
(setq chime-modeline-lookahead-minutes 0)
(chime--update-modeline events)
;; Should clear modeline
(should-not chime-modeline-string)))))
(test-chime-update-modeline-teardown)))
(ert-deftest test-chime-update-modeline-normal-disabled-clears-modeline ()
"Test that chime-enable-modeline nil clears modeline even with valid event.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let* ((now (test-time-today-at 14 0))
(event-time (test-time-today-at 14 10))
(timestamp-str (test-timestamp-string event-time)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* ((event `((times . ((,timestamp-str . ,event-time)))
(title . "Team Meeting")))
(events (list event)))
(setq chime-enable-modeline nil)
(chime--update-modeline events)
;; Should NOT set modeline string when disabled
(should-not chime-modeline-string)))))
(test-chime-update-modeline-teardown)))
(ert-deftest test-chime-update-modeline-normal-enabled-updates-modeline ()
"Test that chime-enable-modeline t allows normal modeline updates.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let* ((now (test-time-today-at 14 0))
(event-time (test-time-today-at 14 10))
(timestamp-str (test-timestamp-string event-time)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* ((event `((times . ((,timestamp-str . ,event-time)))
(title . "Team Meeting")))
(events (list event)))
(setq chime-enable-modeline t)
(chime--update-modeline events)
;; Should set modeline string when enabled
(should chime-modeline-string)
(should (string-match-p "Team Meeting" chime-modeline-string))))))
(test-chime-update-modeline-teardown)))
;;; Boundary Cases
(ert-deftest test-chime-update-modeline-no-events-clears-modeline ()
"Test that no events clears modeline.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let ((now (test-time-today-at 14 0)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let ((events '()))
(chime--update-modeline events)
(should-not chime-modeline-string)))))
(test-chime-update-modeline-teardown)))
(ert-deftest test-chime-update-modeline-day-wide-events-filtered-out ()
"Test that day-wide events are filtered out.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let* ((now (test-time-today-at 14 0))
(event-time (test-time-today-at 0 0))
(timestamp-str (test-timestamp-string event-time t))) ; Day-wide
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* ((event `((times . ((,timestamp-str . ,event-time)))
(title . "All Day Event")))
(events (list event)))
(chime--update-modeline events)
(should-not chime-modeline-string)))))
(test-chime-update-modeline-teardown)))
(ert-deftest test-chime-update-modeline-event-at-exact-boundary-included ()
"Test that event at exact lookahead boundary is included.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let* ((now (test-time-today-at 14 0))
;; Event at 14:30 (exactly 30 minutes, at boundary)
(event-time (test-time-today-at 14 30))
(timestamp-str (test-timestamp-string event-time)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* ((event `((times . ((,timestamp-str . ,event-time)))
(title . "Boundary Event")))
(events (list event)))
(chime--update-modeline events)
(should chime-modeline-string)
(should (string-match-p "Boundary Event" chime-modeline-string))))))
(test-chime-update-modeline-teardown)))
(ert-deftest test-chime-update-modeline-boundary-disabled-overrides-lookahead ()
"Test that chime-enable-modeline nil overrides positive lookahead.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let* ((now (test-time-today-at 14 0))
(event-time (test-time-today-at 14 10))
(timestamp-str (test-timestamp-string event-time)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* ((event `((times . ((,timestamp-str . ,event-time)))
(title . "Team Meeting")))
(events (list event)))
;; Even with positive lookahead, disabled should prevent updates
(setq chime-enable-modeline nil)
(setq chime-modeline-lookahead-minutes 30)
(chime--update-modeline events)
(should-not chime-modeline-string)))))
(test-chime-update-modeline-teardown)))
;;; Error Cases
(ert-deftest test-chime-update-modeline-past-events-not-shown ()
"Test that past events are not shown in modeline.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let* ((now (test-time-today-at 14 0))
;; Event at 13:50 (10 minutes ago)
(event-time (test-time-today-at 13 50))
(timestamp-str (test-timestamp-string event-time)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* ((event `((times . ((,timestamp-str . ,event-time)))
(title . "Past Event")))
(events (list event)))
(chime--update-modeline events)
(should-not chime-modeline-string)))))
(test-chime-update-modeline-teardown)))
(ert-deftest test-chime-update-modeline-error-nil-events-handles-gracefully ()
"Test that nil events parameter doesn't crash.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let ((now (test-time-today-at 14 0)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
;; Should not error with nil events
(should-not (condition-case nil
(progn (chime--update-modeline nil) nil)
(error t)))
;; Modeline should remain unset or cleared
(should-not chime-modeline-string))))
(test-chime-update-modeline-teardown)))
(ert-deftest test-chime-update-modeline-error-invalid-event-structure-handles-gracefully ()
"Test that invalid event structure doesn't crash.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let ((now (test-time-today-at 14 0)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* (;; Event missing required fields
(invalid-event '((invalid . "structure")))
(events (list invalid-event)))
;; Should not crash even with invalid events
(should-not (condition-case nil
(progn (chime--update-modeline events) nil)
(error t)))))))
(test-chime-update-modeline-teardown)))
(ert-deftest test-chime-update-modeline-error-event-with-nil-times-handles-gracefully ()
"Test that event with nil times field doesn't crash.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let ((now (test-time-today-at 14 0)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* ((event '((times . nil)
(title . "Event with nil times")))
(events (list event)))
;; Should not crash
(should-not (condition-case nil
(progn (chime--update-modeline events) nil)
(error t)))
;; Modeline should not be set
(should-not chime-modeline-string)))))
(test-chime-update-modeline-teardown)))
;;; Upcoming Events State Tests
(ert-deftest test-chime-update-modeline-upcoming-events-populated ()
"Test that chime--upcoming-events is populated with all events in window.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let* ((now (test-time-today-at 14 0))
;; Three events within 30 minute window
(event1-time (test-time-today-at 14 5))
(timestamp-str-1 (test-timestamp-string event1-time))
(event2-time (test-time-today-at 14 10))
(timestamp-str-2 (test-timestamp-string event2-time))
(event3-time (test-time-today-at 14 25))
(timestamp-str-3 (test-timestamp-string event3-time)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* ((event1 `((times . ((,timestamp-str-1 . ,event1-time)))
(title . "Event 1")
(marker . nil)))
(event2 `((times . ((,timestamp-str-2 . ,event2-time)))
(title . "Event 2")
(marker . nil)))
(event3 `((times . ((,timestamp-str-3 . ,event3-time)))
(title . "Event 3")
(marker . nil)))
(events (list event1 event2 event3)))
(chime--update-modeline events)
;; Should populate chime--upcoming-events
(should chime--upcoming-events)
;; Should have all 3 events
(should (= 3 (length chime--upcoming-events)))))))
(test-chime-update-modeline-teardown)))
(ert-deftest test-chime-update-modeline-upcoming-events-sorted ()
"Test that chime--upcoming-events are sorted by time (soonest first).
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let* ((now (test-time-today-at 14 0))
;; Add events in reverse order
(event1-time (test-time-today-at 14 25))
(timestamp-str-1 (test-timestamp-string event1-time))
(event2-time (test-time-today-at 14 10))
(timestamp-str-2 (test-timestamp-string event2-time))
(event3-time (test-time-today-at 14 5))
(timestamp-str-3 (test-timestamp-string event3-time)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* ((event1 `((times . ((,timestamp-str-1 . ,event1-time)))
(title . "Latest Event")
(marker . nil)))
(event2 `((times . ((,timestamp-str-2 . ,event2-time)))
(title . "Middle Event")
(marker . nil)))
(event3 `((times . ((,timestamp-str-3 . ,event3-time)))
(title . "Soonest Event")
(marker . nil)))
(events (list event1 event2 event3)))
(chime--update-modeline events)
;; First event should be soonest
(let* ((first-event (car chime--upcoming-events))
(first-event-obj (car first-event))
(first-title (cdr (assoc 'title first-event-obj))))
(should (string= "Soonest Event" first-title)))))))
(test-chime-update-modeline-teardown)))
(ert-deftest test-chime-update-modeline-upcoming-events-cleared-when-disabled ()
"Test that chime--upcoming-events is cleared when modeline disabled.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let* ((now (test-time-today-at 14 0))
(event-time (test-time-today-at 14 10))
(timestamp-str (test-timestamp-string event-time)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* ((event `((times . ((,timestamp-str . ,event-time)))
(title . "Test Event")
(marker . nil)))
(events (list event)))
;; First populate with modeline enabled
(setq chime-enable-modeline t)
(chime--update-modeline events)
(should chime--upcoming-events)
;; Now disable modeline
(setq chime-enable-modeline nil)
(chime--update-modeline events)
;; Should clear chime--upcoming-events
(should-not chime--upcoming-events)))))
(test-chime-update-modeline-teardown)))
(ert-deftest test-chime-update-modeline-upcoming-events-only-within-window ()
"Test that only events within lookahead window are stored.
REFACTORED: Uses dynamic timestamps and with-test-time"
(test-chime-update-modeline-setup)
(unwind-protect
(let* ((now (test-time-today-at 14 0))
;; Event within window (10 minutes)
(event1-time (test-time-today-at 14 10))
(timestamp-str-1 (test-timestamp-string event1-time))
;; Event outside window (60 minutes, window is 30)
(event2-time (test-time-today-at 15 0))
(timestamp-str-2 (test-timestamp-string event2-time)))
(with-test-time now
(cl-letf (((symbol-function 'force-mode-line-update) (lambda (&optional _))))
(let* ((event1 `((times . ((,timestamp-str-1 . ,event1-time)))
(title . "Within Window")
(marker . nil)))
(event2 `((times . ((,timestamp-str-2 . ,event2-time)))
(title . "Outside Window")
(marker . nil)))
(events (list event1 event2)))
(setq chime-modeline-lookahead-minutes 30)
(setq chime-tooltip-lookahead-hours 0.5) ; Also set tooltip lookahead
(chime--update-modeline events)
;; Should only have 1 event (within window)
(should (= 1 (length chime--upcoming-events)))))))
(test-chime-update-modeline-teardown)))
(provide 'test-chime-update-modeline)
;;; test-chime-update-modeline.el ends here
|