aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-10 16:36:47 -0500
committerCraig Jennings <c@cjennings.net>2026-05-10 16:36:47 -0500
commit3c64dac1b98049d475be838294349326c32248ae (patch)
tree626917ca3660d34b48195a77123b1def3d418282 /tests
parent0772736d2bca36472f623d5258784f41db9b4f9a (diff)
downloadchime-3c64dac1b98049d475be838294349326c32248ae.tar.gz
chime-3c64dac1b98049d475be838294349326c32248ae.zip
feat: tighten chime runtime validation and tooltip behavior
Diffstat (limited to 'tests')
-rw-r--r--tests/test-chime-day-wide-time-matching.el74
-rw-r--r--tests/test-chime-debug-functions.el39
-rw-r--r--tests/test-chime-event-contract.el103
-rw-r--r--tests/test-chime-modeline.el142
-rw-r--r--tests/test-chime-refresh-modeline.el92
-rw-r--r--tests/test-chime-tooltip-day-calculation.el358
-rw-r--r--tests/test-chime-validate-configuration.el74
-rw-r--r--tests/test-chime-validation-retry.el574
-rw-r--r--tests/testutil-events.el19
9 files changed, 862 insertions, 613 deletions
diff --git a/tests/test-chime-day-wide-time-matching.el b/tests/test-chime-day-wide-time-matching.el
index 7a7c7d4..88242e2 100644
--- a/tests/test-chime-day-wide-time-matching.el
+++ b/tests/test-chime-day-wide-time-matching.el
@@ -37,6 +37,80 @@
(require 'testutil-general (expand-file-name "testutil-general.el"))
(require 'testutil-time (expand-file-name "testutil-time.el"))
+(defmacro test-chime-with-restored-day-wide-alert-times (&rest body)
+ "Run BODY and restore default `chime-day-wide-alert-times' afterwards."
+ (declare (indent 0) (debug t))
+ `(let ((original-value (default-value 'chime-day-wide-alert-times)))
+ (unwind-protect
+ (progn ,@body)
+ (set-default 'chime-day-wide-alert-times original-value))))
+
+;;;; Tests for chime--validate-day-wide-alert-times
+
+(ert-deftest test-chime-validate-day-wide-alert-times-accepts-24-hour ()
+ "Normal: 24-hour HH:MM entries are valid."
+ (should (equal '("08:00" "17:00")
+ (chime--validate-day-wide-alert-times
+ 'fake-symbol '("08:00" "17:00")))))
+
+(ert-deftest test-chime-validate-day-wide-alert-times-accepts-12-hour ()
+ "Normal: Org-supported 12-hour entries are valid."
+ (should (equal '("8:00am" "5:30pm")
+ (chime--validate-day-wide-alert-times
+ 'fake-symbol '("8:00am" "5:30pm")))))
+
+(ert-deftest test-chime-validate-day-wide-alert-times-accepts-nil ()
+ "Boundary: nil disables day-wide alerts."
+ (should (null (chime--validate-day-wide-alert-times 'fake-symbol nil))))
+
+(ert-deftest test-chime-validate-day-wide-alert-times-accepts-empty-list ()
+ "Boundary: an empty list disables day-wide alerts."
+ (should (equal '()
+ (chime--validate-day-wide-alert-times 'fake-symbol '()))))
+
+(ert-deftest test-chime-validate-day-wide-alert-times-rejects-invalid-string ()
+ "Error: unparseable strings fail before timer matching."
+ (should-error (chime--validate-day-wide-alert-times
+ 'fake-symbol '("08:00" "not-a-time"))
+ :type 'user-error))
+
+(ert-deftest test-chime-validate-day-wide-alert-times-rejects-non-list ()
+ "Error: the value must be nil or a list."
+ (should-error (chime--validate-day-wide-alert-times
+ 'fake-symbol "08:00")
+ :type 'user-error))
+
+(ert-deftest test-chime-validate-day-wide-alert-times-rejects-non-string-entry ()
+ "Error: every configured alert time must be a string."
+ (should-error (chime--validate-day-wide-alert-times
+ 'fake-symbol '("08:00" 1700))
+ :type 'user-error))
+
+(ert-deftest test-chime-validate-day-wide-alert-times-rejects-out-of-day-time ()
+ "Error: Org durations beyond 23:59 are not valid clock times."
+ (should-error (chime--validate-day-wide-alert-times
+ 'fake-symbol '("25:00"))
+ :type 'user-error))
+
+(ert-deftest test-chime-day-wide-alert-times-setter-accepts-valid-list ()
+ "Normal: customize-time setter accepts valid alert times."
+ (test-chime-with-restored-day-wide-alert-times
+ (customize-set-variable 'chime-day-wide-alert-times '("08:00" "5:30pm"))
+ (should (equal '("08:00" "5:30pm") chime-day-wide-alert-times))))
+
+(ert-deftest test-chime-day-wide-alert-times-setter-accepts-nil ()
+ "Boundary: customize-time setter accepts nil."
+ (test-chime-with-restored-day-wide-alert-times
+ (customize-set-variable 'chime-day-wide-alert-times nil)
+ (should (null chime-day-wide-alert-times))))
+
+(ert-deftest test-chime-day-wide-alert-times-setter-rejects-invalid-list ()
+ "Error: customize-time setter rejects invalid alert times."
+ (test-chime-with-restored-day-wide-alert-times
+ (should-error (customize-set-variable
+ 'chime-day-wide-alert-times '("08:00" "nope"))
+ :type 'user-error)))
+
;;;; Tests for chime--current-time-matches-time-of-day-string
;;; Normal Cases
diff --git a/tests/test-chime-debug-functions.el b/tests/test-chime-debug-functions.el
index a01fd5f..7ef85e5 100644
--- a/tests/test-chime-debug-functions.el
+++ b/tests/test-chime-debug-functions.el
@@ -26,6 +26,7 @@
;;; Code:
(setq chime-debug t)
+(require 'cl-lib)
(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
(require 'chime-debug (expand-file-name "../chime-debug.el"))
@@ -46,6 +47,13 @@
(chime-delete-test-base-dir)
(setq chime--upcoming-events nil))
+(defmacro test-chime-debug-functions--without-echo (&rest body)
+ "Run BODY while preserving debug logs without echoing messages."
+ `(cl-letf (((symbol-function 'message)
+ (lambda (format-string &rest args)
+ (apply #'chime--log-silently format-string args))))
+ ,@body))
+
;;; Tests for chime-debug-dump-events
(ert-deftest test-chime-debug-dump-events-normal-with-events ()
@@ -67,7 +75,8 @@
(let ((inhibit-read-only t))
(erase-buffer)))
;; Call debug function
- (chime-debug-dump-events)
+ (test-chime-debug-functions--without-echo
+ (chime-debug-dump-events))
;; Verify output in *Messages* buffer
(with-current-buffer "*Messages*"
(let ((content (buffer-string)))
@@ -85,7 +94,10 @@
(setq chime--upcoming-events nil)
;; Should not error
(should-not (condition-case nil
- (progn (chime-debug-dump-events) nil)
+ (progn
+ (test-chime-debug-functions--without-echo
+ (chime-debug-dump-events))
+ nil)
(error t))))
(test-chime-debug-functions-teardown)))
@@ -110,7 +122,8 @@
(let ((inhibit-read-only t))
(erase-buffer)))
;; Call debug function
- (chime-debug-dump-tooltip)
+ (test-chime-debug-functions--without-echo
+ (chime-debug-dump-tooltip))
;; Verify output in *Messages* buffer
(with-current-buffer "*Messages*"
(let ((content (buffer-string)))
@@ -128,7 +141,10 @@
(setq chime--upcoming-events nil)
;; Should not error
(should-not (condition-case nil
- (progn (chime-debug-dump-tooltip) nil)
+ (progn
+ (test-chime-debug-functions--without-echo
+ (chime-debug-dump-tooltip))
+ nil)
(error t))))
(test-chime-debug-functions-teardown)))
@@ -147,7 +163,8 @@
(let ((inhibit-read-only t))
(erase-buffer)))
;; Call debug function
- (chime-debug-config)
+ (test-chime-debug-functions--without-echo
+ (chime-debug-config))
;; Verify output in *Messages* buffer
(with-current-buffer "*Messages*"
(let ((content (buffer-string)))
@@ -171,7 +188,10 @@
(erase-buffer)))
;; Should not error
(should-not (condition-case nil
- (progn (chime-debug-config) nil)
+ (progn
+ (test-chime-debug-functions--without-echo
+ (chime-debug-config))
+ nil)
(error t)))
;; Verify output mentions 0 files
(with-current-buffer "*Messages*"
@@ -200,9 +220,10 @@
;; Call all three debug functions - should not error
(should-not (condition-case nil
(progn
- (chime-debug-dump-events)
- (chime-debug-dump-tooltip)
- (chime-debug-config)
+ (test-chime-debug-functions--without-echo
+ (chime-debug-dump-events)
+ (chime-debug-dump-tooltip)
+ (chime-debug-config))
nil)
(error t))))))
(test-chime-debug-functions-teardown)))
diff --git a/tests/test-chime-event-contract.el b/tests/test-chime-event-contract.el
new file mode 100644
index 0000000..697dccb
--- /dev/null
+++ b/tests/test-chime-event-contract.el
@@ -0,0 +1,103 @@
+;;; test-chime-event-contract.el --- Tests for Chime event alist contract -*- lexical-binding: t; -*-
+
+;; Copyright (C) 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 the explicit internal event alist contract.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+
+(require 'testutil-general (expand-file-name "testutil-general.el"))
+(require 'testutil-time (expand-file-name "testutil-time.el"))
+(require 'testutil-events (expand-file-name "testutil-events.el"))
+
+(ert-deftest test-chime-event-contract-make-event-creates-valid-event ()
+ "Constructor returns an event matching the documented contract."
+ (let* ((event-time (test-time-tomorrow-at 9 30))
+ (timestamp (test-timestamp-string event-time))
+ (event (chime--make-event
+ (list (cons timestamp event-time))
+ "Planning"
+ '((10 . medium) (0 . high))
+ "/tmp/chime-test.org"
+ 42)))
+ (should (chime--valid-event-p event))
+ (should (equal (list (cons timestamp event-time))
+ (chime--event-times event)))
+ (should (string= "Planning" (chime--event-title event)))
+ (should (equal '((10 . medium) (0 . high))
+ (chime--event-intervals event)))
+ (should (string= "/tmp/chime-test.org"
+ (chime--event-marker-file event)))
+ (should (= 42 (chime--event-marker-pos event)))))
+
+(ert-deftest test-chime-event-contract-validates-all-day-timestamps ()
+ "All-day timestamps are valid when their parsed time value is nil."
+ (let ((event (chime--make-event
+ '(("<2026-05-11 Mon>" . nil))
+ "All Day"
+ '((10 . medium)))))
+ (should (chime--valid-event-p event))))
+
+(ert-deftest test-chime-event-contract-rejects-missing-required-keys ()
+ "Validator rejects event alists missing required keys."
+ (should-not (chime--valid-event-p
+ '((times . nil)
+ (intervals . ((10 . medium)))))))
+
+(ert-deftest test-chime-event-contract-rejects-malformed-time-entry ()
+ "Constructor rejects malformed timestamp entries."
+ (should-error
+ (chime--make-event
+ '((not-a-string . nil))
+ "Bad Time"
+ '((10 . medium)))))
+
+(ert-deftest test-chime-event-contract-rejects-malformed-interval-entry ()
+ "Constructor rejects malformed alert intervals."
+ (should-error
+ (chime--make-event
+ '(("<2026-05-11 Mon 09:30>" . nil))
+ "Bad Interval"
+ '((10 . urgent)))))
+
+(ert-deftest test-chime-event-contract-rejects-malformed-marker-identity ()
+ "Constructor rejects partial or wrongly typed marker identity values."
+ (should-error
+ (chime--make-event
+ '(("<2026-05-11 Mon 09:30>" . nil))
+ "Bad Marker"
+ '((10 . medium))
+ "/tmp/chime-test.org"
+ "42")))
+
+(ert-deftest test-chime-event-contract-test-builder-uses-valid-shape ()
+ "Shared test event builder emits contract-valid event alists."
+ (let ((event (test-make-simple-event
+ "Builder Event"
+ (test-time-tomorrow-at 11 0)
+ 5
+ 'low)))
+ (should (chime--valid-event-p event))
+ (should (string= "Builder Event" (chime--event-title event)))))
+
+(provide 'test-chime-event-contract)
+;;; test-chime-event-contract.el ends here
diff --git a/tests/test-chime-modeline.el b/tests/test-chime-modeline.el
index cb046b5..63ba26c 100644
--- a/tests/test-chime-modeline.el
+++ b/tests/test-chime-modeline.el
@@ -32,6 +32,7 @@
;; Load test utilities
(require 'testutil-general (expand-file-name "testutil-general.el"))
(require 'testutil-time (expand-file-name "testutil-time.el"))
+(require 'testutil-events (expand-file-name "testutil-events.el"))
;;; Setup and Teardown
@@ -204,15 +205,14 @@ REFACTORED: Uses dynamic timestamps"
;; Generate tooltip
(let ((tooltip (chime--make-tooltip chime--upcoming-events)))
- (message "DEBUG: Tooltip content:\n%s" tooltip)
-
- ;; Tooltip should contain "Team Meeting" exactly once
- (let ((count (test-chime-modeline--count-in-string "Team Meeting" tooltip)))
- (should (= 1 count)))
-
- ;; "Upcoming Events" header should appear exactly once
- (let ((header-count (test-chime-modeline--count-in-string "Upcoming Events" tooltip)))
- (should (= 1 header-count))))))
+ (ert-info ((format "Tooltip content:\n%s" tooltip))
+ ;; Tooltip should contain "Team Meeting" exactly once
+ (let ((count (test-chime-modeline--count-in-string "Team Meeting" tooltip)))
+ (should (= 1 count)))
+
+ ;; "Upcoming Events" header should appear exactly once
+ (let ((header-count (test-chime-modeline--count-in-string "Upcoming Events" tooltip)))
+ (should (= 1 header-count)))))))
(test-chime-modeline-teardown)))
(ert-deftest test-chime-modeline-tooltip-correct-order ()
@@ -244,14 +244,13 @@ REFACTORED: Uses dynamic timestamps"
;; Generate tooltip
(let ((tooltip (chime--make-tooltip chime--upcoming-events)))
- (message "DEBUG: Tooltip for order test:\n%s" tooltip)
-
- ;; "Meeting A" should appear before "Meeting B" in tooltip
- (let ((pos-a (string-match "Meeting A" tooltip))
- (pos-b (string-match "Meeting B" tooltip)))
- (should pos-a)
- (should pos-b)
- (should (< pos-a pos-b))))))
+ (ert-info ((format "Tooltip content:\n%s" tooltip))
+ ;; "Meeting A" should appear before "Meeting B" in tooltip
+ (let ((pos-a (string-match "Meeting A" tooltip))
+ (pos-b (string-match "Meeting B" tooltip)))
+ (should pos-a)
+ (should pos-b)
+ (should (< pos-a pos-b)))))))
(test-chime-modeline-teardown)))
(ert-deftest test-chime-modeline-tooltip-structure ()
@@ -283,18 +282,113 @@ REFACTORED: Uses dynamic timestamps"
;; Generate tooltip
(let ((tooltip (chime--make-tooltip chime--upcoming-events)))
- (message "DEBUG: Tooltip structure:\n%s" tooltip)
+ (ert-info ((format "Tooltip content:\n%s" tooltip))
+ ;; Should have exactly one "Upcoming Events" header
+ (should (= 1 (test-chime-modeline--count-in-string "Upcoming Events" tooltip)))
- ;; Should have exactly one "Upcoming Events" header
- (should (= 1 (test-chime-modeline--count-in-string "Upcoming Events" tooltip)))
+ ;; Should start with "Upcoming Events as of" (new header format with timestamp)
+ (should (string-match-p "^Upcoming Events as of" tooltip))
- ;; Should start with "Upcoming Events as of" (new header format with timestamp)
- (should (string-match-p "^Upcoming Events as of" tooltip))
+ ;; Event should appear exactly once
+ (should (= 1 (test-chime-modeline--count-in-string "Team Meeting" tooltip)))))))
+ (test-chime-modeline-teardown)))
- ;; Event should appear exactly once
- (should (= 1 (test-chime-modeline--count-in-string "Team Meeting" tooltip))))))
+(ert-deftest test-chime-modeline-tooltip-custom-event-format ()
+ "Tooltip event line uses chime-tooltip-event-format."
+ (test-chime-modeline-setup)
+ (unwind-protect
+ (let* ((now (test-time-now))
+ (event-time (test-time-tomorrow-at 14 0))
+ (event (test-make-simple-event "Team Meeting" event-time))
+ (upcoming (list (list event
+ (cons (test-timestamp-string event-time)
+ event-time)
+ 1440)))
+ (chime-tooltip-event-format "%T -- %t -- %u"))
+ (with-test-time now
+ (let ((tooltip (chime--make-tooltip upcoming)))
+ (should (string-match-p "02:00 PM -- Team Meeting -- (in 1 day)" tooltip)))))
(test-chime-modeline-teardown)))
+(ert-deftest test-chime-modeline-tooltip-custom-day-labels ()
+ "Tooltip day labels use custom today, tomorrow, and future formats."
+ (test-chime-modeline-setup)
+ (unwind-protect
+ (let* ((now (test-time-today-at 9 0))
+ (today (time-add now (seconds-to-time (* 2 3600))))
+ (tomorrow (time-add now (days-to-time 1)))
+ (future (time-add now (days-to-time 3)))
+ (chime-tooltip-today-label "Hoy")
+ (chime-tooltip-tomorrow-label "Manana")
+ (chime-tooltip-relative-day-format "%s :: %Y-%m-%d")
+ (chime-tooltip-future-day-format "Dia %Y-%m-%d"))
+ (with-test-time now
+ (should (string-match-p "^Hoy :: "
+ (chime--day-label-for-event-time
+ today now tomorrow)))
+ (should (string-match-p "^Manana :: "
+ (chime--day-label-for-event-time
+ tomorrow now tomorrow)))
+ (should (string-match-p "^Dia "
+ (chime--day-label-for-event-time
+ future now tomorrow)))))
+ (test-chime-modeline-teardown)))
+
+(ert-deftest test-chime-modeline-tooltip-custom-overflow-and-separator ()
+ "Tooltip overflow and section separator strings are customizable."
+ (test-chime-modeline-setup)
+ (unwind-protect
+ (let* ((now (test-time-now))
+ (first-time (test-time-tomorrow-at 14 0))
+ (second-time (test-time-tomorrow-at 15 0))
+ (first-event (test-make-simple-event "First Event" first-time))
+ (second-event (test-make-simple-event "Second Event" second-time))
+ (upcoming (list (list first-event
+ (cons (test-timestamp-string first-time)
+ first-time)
+ 1440)
+ (list second-event
+ (cons (test-timestamp-string second-time)
+ second-time)
+ 1500)))
+ (chime-modeline-tooltip-max-events 1)
+ (chime-tooltip-section-separator "---")
+ (chime-tooltip-more-events-format "plus %d hidden%s"))
+ (with-test-time now
+ (let ((tooltip (chime--make-tooltip upcoming)))
+ (should (string-match-p "---" tooltip))
+ (should (string-match-p "plus 1 hidden" tooltip))
+ (should-not (string-match-p "Second Event" tooltip)))))
+ (test-chime-modeline-teardown)))
+
+(ert-deftest test-chime-modeline-tooltip-custom-no-events-text ()
+ "No-events tooltip guidance strings are customizable."
+ (test-chime-modeline-setup)
+ (unwind-protect
+ (let ((chime-tooltip-header-format "Agenda %Y")
+ (chime-tooltip-no-events-separator "---")
+ (chime-tooltip-no-events-format "Nada por %s.")
+ (chime-tooltip-increase-lookahead-format "Aumenta %s.")
+ (chime-tooltip-left-click-label "Click: calendario"))
+ (let ((tooltip (chime--make-no-events-tooltip 120)))
+ (should (string-match-p "^Agenda " tooltip))
+ (should (string-match-p "---" tooltip))
+ (should (string-match-p "Nada por 2 hours\\." tooltip))
+ (should (string-match-p "Aumenta chime-tooltip-lookahead-hours\\." tooltip))
+ (should (string-match-p "Click: calendario" tooltip))))
+ (test-chime-modeline-teardown)))
+
+(ert-deftest test-chime-modeline-tooltip-custom-day-hour-countdown ()
+ "Tooltip-specific day/hour countdown units are customizable."
+ (let ((chime-tooltip-countdown-prefix "en")
+ (chime-tooltip-day-unit-labels '("dia" . "dias"))
+ (chime-tooltip-hour-unit-labels '("hora" . "horas")))
+ (should (string-match-p "(en 2 dias 1 hora)"
+ (chime--format-event-for-tooltip
+ "<2026-05-12 Tue 10:00>"
+ 2940
+ "Evento")))))
+
;;; Tests for tooltip max events limit
(ert-deftest test-chime-modeline-tooltip-max-events ()
diff --git a/tests/test-chime-refresh-modeline.el b/tests/test-chime-refresh-modeline.el
new file mode 100644
index 0000000..1001b06
--- /dev/null
+++ b/tests/test-chime-refresh-modeline.el
@@ -0,0 +1,92 @@
+;;; test-chime-refresh-modeline.el --- Tests for manual modeline refresh -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;;; Commentary:
+
+;; Tests for `chime-refresh-modeline'. Manual refresh should share the
+;; startup validation gate used by `chime-check', but it must remain a
+;; modeline-only operation and never send notifications.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+
+(defmacro test-chime-refresh-modeline--with-validation-state (&rest body)
+ "Run BODY with isolated chime validation state."
+ (declare (indent 0) (debug t))
+ `(let ((original-validation-done chime--validation-done)
+ (original-validation-retry-count chime--validation-retry-count)
+ (original-validation-max-retries chime--validation-max-retries)
+ (original-org-agenda-files org-agenda-files))
+ (unwind-protect
+ (progn
+ (setq chime--validation-done nil)
+ (setq chime--validation-retry-count 0)
+ (setq chime--validation-max-retries 3)
+ ,@body)
+ (setq chime--validation-done original-validation-done)
+ (setq chime--validation-retry-count original-validation-retry-count)
+ (setq chime--validation-max-retries original-validation-max-retries)
+ (setq org-agenda-files original-org-agenda-files))))
+
+(ert-deftest test-chime-refresh-modeline-nil-agenda-files-skips-fetch ()
+ "Error: nil `org-agenda-files' should validate and skip fetch."
+ (test-chime-refresh-modeline--with-validation-state
+ (setq org-agenda-files nil)
+ (let ((fetch-called nil)
+ (messages nil))
+ (cl-letf (((symbol-function 'chime--fetch-and-process)
+ (lambda (_callback)
+ (setq fetch-called t)))
+ ((symbol-function 'message)
+ (lambda (format-string &rest args)
+ (push (apply #'format format-string args) messages))))
+ (chime-refresh-modeline)
+ (should-not fetch-called)
+ (should (= 1 chime--validation-retry-count))
+ (should (cl-some (lambda (msg)
+ (string-match-p "Waiting for org-agenda-files" msg))
+ messages))))))
+
+(ert-deftest test-chime-refresh-modeline-empty-agenda-files-skips-fetch ()
+ "Error: empty `org-agenda-files' should validate and skip fetch."
+ (test-chime-refresh-modeline--with-validation-state
+ (setq org-agenda-files '())
+ (let ((fetch-called nil))
+ (cl-letf (((symbol-function 'chime--fetch-and-process)
+ (lambda (_callback)
+ (setq fetch-called t)))
+ ((symbol-function 'message)
+ (lambda (&rest _args) nil)))
+ (chime-refresh-modeline)
+ (should-not fetch-called)
+ (should (= 1 chime--validation-retry-count))
+ (should-not chime--validation-done)))))
+
+(ert-deftest test-chime-refresh-modeline-valid-agenda-files-fetches-events ()
+ "Normal: valid configuration should fetch and update the modeline."
+ (test-chime-refresh-modeline--with-validation-state
+ (setq org-agenda-files '("/tmp/chime-refresh-test.org"))
+ (let ((update-called nil)
+ (notifications-called nil)
+ (events '(((title . "Meeting")
+ (times . nil)
+ (intervals . nil)))))
+ (cl-letf (((symbol-function 'chime--fetch-and-process)
+ (lambda (callback)
+ (funcall callback events)))
+ ((symbol-function 'chime--update-modeline)
+ (lambda (received-events)
+ (setq update-called received-events)))
+ ((symbol-function 'chime--process-notifications)
+ (lambda (_events)
+ (setq notifications-called t))))
+ (chime-refresh-modeline)
+ (should (eq update-called events))
+ (should-not notifications-called)
+ (should chime--validation-done)
+ (should (= 0 chime--validation-retry-count))))))
+
+(provide 'test-chime-refresh-modeline)
+;;; test-chime-refresh-modeline.el ends here
diff --git a/tests/test-chime-tooltip-day-calculation.el b/tests/test-chime-tooltip-day-calculation.el
index 1a25767..6e4f778 100644
--- a/tests/test-chime-tooltip-day-calculation.el
+++ b/tests/test-chime-tooltip-day-calculation.el
@@ -15,6 +15,18 @@
(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
(require 'testutil-time (expand-file-name "testutil-time.el"))
(require 'testutil-general (expand-file-name "testutil-general.el"))
+(require 'testutil-events (expand-file-name "testutil-events.el"))
+
+(defmacro test-chime-tooltip-day-calculation--with-tooltip (now content &rest body)
+ "Bind tooltip for CONTENT at NOW and run BODY with common test config."
+ (declare (indent 2))
+ `(with-test-setup
+ (with-chime-config
+ chime-modeline-lookahead-minutes 10080
+ chime-tooltip-lookahead-hours 168
+ (with-test-time ,now
+ (with-chime-tooltip-from-content ,content tooltip
+ ,@body)))))
(ert-deftest test-chime-tooltip-day-calculation-fractional-days ()
"Test that fractional days show both days and hours correctly.
@@ -24,65 +36,36 @@ User scenario: Viewing tooltip on Sunday 9pm, sees:
- Wednesday 2pm event: 65 hours = 2.7 days → 'in 2 days 17 hours'
This test prevents regression of the integer division truncation bug."
- (chime-create-test-base-dir)
- (unwind-protect
- (let* ((now (test-time-today-at 21 0)) ; Sunday 9pm
- ;; Create events at specific future times
- (tuesday-9pm (time-add now (seconds-to-time (* 48 3600)))) ; +48 hours
- (wednesday-2pm (time-add now (seconds-to-time (* 65 3600)))) ; +65 hours
- (content (format "* Tuesday Event\n<%s>\n* Wednesday Event\n<%s>\n"
- (format-time-string "<%Y-%m-%d %a %H:%M>" tuesday-9pm)
- (format-time-string "<%Y-%m-%d %a %H:%M>" wednesday-2pm)))
- (test-file (chime-create-temp-test-file-with-content content))
- (test-buffer (find-file-noselect test-file))
- (events nil))
-
- ;; Gather events
- (with-current-buffer test-buffer
- (org-mode)
- (goto-char (point-min))
- (while (re-search-forward "^\\*+ " nil t)
- (beginning-of-line)
- (push (chime--gather-info (point-marker)) events)
- (forward-line 1)))
- (kill-buffer test-buffer)
- (setq events (nreverse events))
-
- ;; Set lookahead to cover events (7 days)
- (setq chime-modeline-lookahead-minutes 10080)
- (setq chime-tooltip-lookahead-hours 168)
-
- (with-test-time now
- ;; Update modeline and get tooltip
- (chime--update-modeline events)
- (let ((tooltip (chime--make-tooltip chime--upcoming-events)))
-
- ;; Verify tooltip contains both events
- (should (string-match-p "Tuesday Event" tooltip))
- (should (string-match-p "Wednesday Event" tooltip))
-
- ;; Print tooltip for manual inspection
- (message "TOOLTIP CONTENT:\n%s" tooltip)
-
- ;; AFTER FIX: Tuesday shows "in 2 days", Wednesday shows "in 2 days 17 hours"
- ;; Verify Tuesday shows exactly 2 days (no "hours" in countdown)
- (should (string-match-p "Tuesday Event.*(in 2 days)" tooltip))
- ;; Make sure Tuesday doesn't have hours
- (should-not (string-match-p "Tuesday Event.*hours" tooltip))
-
- ;; Verify Wednesday shows 2 days AND 17 hours
- (should (string-match-p "Wednesday Event.*(in 2 days 17 hours)" tooltip))
-
- ;; Verify they show DIFFERENT countdowns
- (let ((tuesday-line (progn
- (string-match "Tuesday Event[^\n]*" tooltip)
- (match-string 0 tooltip)))
- (wednesday-line (progn
- (string-match "Wednesday Event[^\n]*" tooltip)
- (match-string 0 tooltip))))
- (should-not (string= tuesday-line wednesday-line))))))
-
- (chime-delete-test-base-dir)))
+ (let* ((now (test-time-today-at 21 0)) ; Sunday 9pm
+ ;; Create events at specific future times
+ (tuesday-9pm (time-add now (seconds-to-time (* 48 3600)))) ; +48 hours
+ (wednesday-2pm (time-add now (seconds-to-time (* 65 3600)))) ; +65 hours
+ (content (format "* Tuesday Event\n<%s>\n* Wednesday Event\n<%s>\n"
+ (format-time-string "<%Y-%m-%d %a %H:%M>" tuesday-9pm)
+ (format-time-string "<%Y-%m-%d %a %H:%M>" wednesday-2pm))))
+ (test-chime-tooltip-day-calculation--with-tooltip now content
+ (ert-info ((format "Tooltip content:\n%s" tooltip))
+ ;; Verify tooltip contains both events
+ (should (string-match-p "Tuesday Event" tooltip))
+ (should (string-match-p "Wednesday Event" tooltip))
+
+ ;; AFTER FIX: Tuesday shows "in 2 days", Wednesday shows "in 2 days 17 hours"
+ ;; Verify Tuesday shows exactly 2 days (no "hours" in countdown)
+ (should (string-match-p "Tuesday Event.*(in 2 days)" tooltip))
+ ;; Make sure Tuesday doesn't have hours
+ (should-not (string-match-p "Tuesday Event.*hours" tooltip))
+
+ ;; Verify Wednesday shows 2 days AND 17 hours
+ (should (string-match-p "Wednesday Event.*(in 2 days 17 hours)" tooltip))
+
+ ;; Verify they show DIFFERENT countdowns
+ (let ((tuesday-line (progn
+ (string-match "Tuesday Event[^\n]*" tooltip)
+ (match-string 0 tooltip)))
+ (wednesday-line (progn
+ (string-match "Wednesday Event[^\n]*" tooltip)
+ (match-string 0 tooltip))))
+ (should-not (string= tuesday-line wednesday-line)))))))
;;; Helper function for creating test events
@@ -103,116 +86,47 @@ Returns formatted org content string."
(ert-deftest test-chime-tooltip-day-calculation-boundary-exactly-24-hours ()
"Test event exactly 24 hours away shows 'in 1 day' not hours."
- (chime-create-test-base-dir)
- (unwind-protect
- (let* ((now (test-time-today-at 12 0))
- (content (test-chime-tooltip-day-calculation--create-event-at-hours now "Tomorrow Same Time" 24))
- (test-file (chime-create-temp-test-file-with-content content))
- (events (with-current-buffer (find-file-noselect test-file)
- (org-mode)
- (goto-char (point-min))
- (let ((evs nil))
- (while (re-search-forward "^\\*+ " nil t)
- (push (chime--gather-info (point-marker)) evs))
- (nreverse evs)))))
- (kill-buffer (get-file-buffer test-file))
-
- (setq chime-modeline-lookahead-minutes 10080)
- (setq chime-tooltip-lookahead-hours 168)
-
- (with-test-time now
- (chime--update-modeline events)
- (let ((tooltip (chime--make-tooltip chime--upcoming-events)))
- ;; Should show "in 1 day" not hours
- (should (string-match-p "(in 1 day)" tooltip))
- (should-not (string-match-p "hours" tooltip)))))
- (chime-delete-test-base-dir)))
+ (let* ((now (test-time-today-at 12 0))
+ (content (test-chime-tooltip-day-calculation--create-event-at-hours
+ now "Tomorrow Same Time" 24)))
+ (test-chime-tooltip-day-calculation--with-tooltip now content
+ ;; Should show "in 1 day" not hours
+ (should (string-match-p "(in 1 day)" tooltip))
+ (should-not (string-match-p "hours" tooltip)))))
(ert-deftest test-chime-tooltip-day-calculation-boundary-23-hours-59-minutes ()
"Test event 23h59m away shows hours, not days (just under 24h threshold)."
- (chime-create-test-base-dir)
- (unwind-protect
- (let* ((now (test-time-today-at 12 0))
- ;; 23 hours 59 minutes = 1439 minutes = just under 1440
- (event-time (time-add now (seconds-to-time (* 1439 60))))
- (content (format "* Almost Tomorrow\n<%s>\n"
- (format-time-string "%Y-%m-%d %a %H:%M" event-time)))
- (test-file (chime-create-temp-test-file-with-content content))
- (events (with-current-buffer (find-file-noselect test-file)
- (org-mode)
- (goto-char (point-min))
- (let ((evs nil))
- (while (re-search-forward "^\\*+ " nil t)
- (push (chime--gather-info (point-marker)) evs))
- (nreverse evs)))))
- (kill-buffer (get-file-buffer test-file))
-
- (setq chime-modeline-lookahead-minutes 10080)
- (setq chime-tooltip-lookahead-hours 168)
-
- (with-test-time now
- (chime--update-modeline events)
- (let ((tooltip (chime--make-tooltip chime--upcoming-events)))
- ;; Should show hours format (< 24 hours)
- (should (string-match-p "hours" tooltip))
- (should-not (string-match-p "days?" tooltip)))))
- (chime-delete-test-base-dir)))
+ (let* ((now (test-time-today-at 12 0))
+ ;; 23 hours 59 minutes = 1439 minutes = just under 1440
+ (event-time (time-add now (seconds-to-time (* 1439 60))))
+ (content (format "* Almost Tomorrow\n<%s>\n"
+ (format-time-string "%Y-%m-%d %a %H:%M" event-time))))
+ (test-chime-tooltip-day-calculation--with-tooltip now content
+ ;; Should show hours format (< 24 hours)
+ (should (string-match-p "hours" tooltip))
+ (should-not (string-match-p "days?" tooltip)))))
(ert-deftest test-chime-tooltip-day-calculation-boundary-25-hours ()
"Test event 25 hours away shows 'in 1 day 1 hour'."
- (chime-create-test-base-dir)
- (unwind-protect
- (let* ((now (test-time-today-at 12 0))
- (content (test-chime-tooltip-day-calculation--create-event-at-hours now "Day Plus One" 25))
- (test-file (chime-create-temp-test-file-with-content content))
- (events (with-current-buffer (find-file-noselect test-file)
- (org-mode)
- (goto-char (point-min))
- (let ((evs nil))
- (while (re-search-forward "^\\*+ " nil t)
- (push (chime--gather-info (point-marker)) evs))
- (nreverse evs)))))
- (kill-buffer (get-file-buffer test-file))
-
- (setq chime-modeline-lookahead-minutes 10080)
- (setq chime-tooltip-lookahead-hours 168)
-
- (with-test-time now
- (chime--update-modeline events)
- (let ((tooltip (chime--make-tooltip chime--upcoming-events)))
- ;; Should show "in 1 day 1 hour"
- (should (string-match-p "(in 1 day 1 hour)" tooltip)))))
- (chime-delete-test-base-dir)))
+ (let* ((now (test-time-today-at 12 0))
+ (content (test-chime-tooltip-day-calculation--create-event-at-hours
+ now "Day Plus One" 25)))
+ (test-chime-tooltip-day-calculation--with-tooltip now content
+ ;; Should show "in 1 day 1 hour"
+ (should (string-match-p "(in 1 day 1 hour)" tooltip)))))
(ert-deftest test-chime-tooltip-day-calculation-boundary-exactly-48-hours ()
"Test event exactly 48 hours away shows 'in 2 days' without hours."
- (chime-create-test-base-dir)
- (unwind-protect
- (let* ((now (test-time-today-at 12 0))
- (content (test-chime-tooltip-day-calculation--create-event-at-hours now "Two Days Exact" 48))
- (test-file (chime-create-temp-test-file-with-content content))
- (events (with-current-buffer (find-file-noselect test-file)
- (org-mode)
- (goto-char (point-min))
- (let ((evs nil))
- (while (re-search-forward "^\\*+ " nil t)
- (push (chime--gather-info (point-marker)) evs))
- (nreverse evs)))))
- (kill-buffer (get-file-buffer test-file))
-
- (setq chime-modeline-lookahead-minutes 10080)
- (setq chime-tooltip-lookahead-hours 168)
-
- (with-test-time now
- (chime--update-modeline events)
- (let ((tooltip (chime--make-tooltip chime--upcoming-events))
- (line (test-chime-tooltip-day-calculation--get-formatted-line
- (chime--make-tooltip chime--upcoming-events) "Two Days Exact")))
- ;; Should show exactly "in 2 days" with NO hours
- (should (string-match-p "(in 2 days)" tooltip))
- ;; Verify the line doesn't contain "hour" (would be "2 days 0 hours")
- (should-not (string-match-p "hour" line)))))
- (chime-delete-test-base-dir)))
+ (let* ((now (test-time-today-at 12 0))
+ (content (test-chime-tooltip-day-calculation--create-event-at-hours
+ now "Two Days Exact" 48)))
+ (test-chime-tooltip-day-calculation--with-tooltip now content
+ (let ((line (test-chime-tooltip-day-calculation--get-formatted-line
+ tooltip "Two Days Exact")))
+ ;; Should show exactly "in 2 days" with NO hours
+ (should (string-match-p "(in 2 days)" tooltip))
+ ;; Verify the line doesn't contain "hour" (would be "2 days 0 hours")
+ (should-not (string-match-p "hour" line))))))
;;; Midnight Boundaries
@@ -221,102 +135,50 @@ Returns formatted org content string."
Scenario: 11pm now, event at 2am (3 hours later, next calendar day)
Should show hours, not '1 day' since it's only 3 hours away."
- (chime-create-test-base-dir)
- (unwind-protect
- (let* ((now (test-time-today-at 23 0)) ; 11pm
- ;; 3 hours later = 2am next day
- (content (test-chime-tooltip-day-calculation--create-event-at-hours now "Early Morning" 3))
- (test-file (chime-create-temp-test-file-with-content content))
- (events (with-current-buffer (find-file-noselect test-file)
- (org-mode)
- (goto-char (point-min))
- (let ((evs nil))
- (while (re-search-forward "^\\*+ " nil t)
- (push (chime--gather-info (point-marker)) evs))
- (nreverse evs)))))
- (kill-buffer (get-file-buffer test-file))
-
- (setq chime-modeline-lookahead-minutes 10080)
- (setq chime-tooltip-lookahead-hours 168)
-
- (with-test-time now
- (chime--update-modeline events)
- (let ((tooltip (chime--make-tooltip chime--upcoming-events)))
- ;; Should show "in 3 hours" not "in 1 day"
- (should (string-match-p "3 hours" tooltip))
- (should-not (string-match-p "days?" tooltip)))))
- (chime-delete-test-base-dir)))
+ (let* ((now (test-time-today-at 23 0)) ; 11pm
+ ;; 3 hours later = 2am next day
+ (content (test-chime-tooltip-day-calculation--create-event-at-hours
+ now "Early Morning" 3)))
+ (test-chime-tooltip-day-calculation--with-tooltip now content
+ ;; Should show "in 3 hours" not "in 1 day"
+ (should (string-match-p "3 hours" tooltip))
+ (should-not (string-match-p "days?" tooltip)))))
(ert-deftest test-chime-tooltip-day-calculation-midnight-plus-one-day ()
"Test event at midnight tomorrow (24h exactly) shows '1 day'."
- (chime-create-test-base-dir)
- (unwind-protect
- (let* ((now (test-time-today-at 0 0)) ; Midnight today
- (content (test-chime-tooltip-day-calculation--create-event-at-hours now "Midnight Tomorrow" 24))
- (test-file (chime-create-temp-test-file-with-content content))
- (events (with-current-buffer (find-file-noselect test-file)
- (org-mode)
- (goto-char (point-min))
- (let ((evs nil))
- (while (re-search-forward "^\\*+ " nil t)
- (push (chime--gather-info (point-marker)) evs))
- (nreverse evs)))))
- (kill-buffer (get-file-buffer test-file))
-
- (setq chime-modeline-lookahead-minutes 10080)
- (setq chime-tooltip-lookahead-hours 168)
-
- (with-test-time now
- (chime--update-modeline events)
- (let ((tooltip (chime--make-tooltip chime--upcoming-events)))
- (should (string-match-p "(in 1 day)" tooltip))
- (should-not (string-match-p "hour" tooltip)))))
- (chime-delete-test-base-dir)))
+ (let* ((now (test-time-today-at 0 0)) ; Midnight today
+ (content (test-chime-tooltip-day-calculation--create-event-at-hours
+ now "Midnight Tomorrow" 24)))
+ (test-chime-tooltip-day-calculation--with-tooltip now content
+ (should (string-match-p "(in 1 day)" tooltip))
+ (should-not (string-match-p "hour" tooltip)))))
;;; Multiple Events - Verify distinct formatting
(ert-deftest test-chime-tooltip-day-calculation-multiple-events-distinct ()
"Test multiple events at different fractional-day offsets show distinct times."
- (chime-create-test-base-dir)
- (unwind-protect
- (let* ((now (test-time-today-at 12 0))
- (content (concat
- (test-chime-tooltip-day-calculation--create-event-at-hours now "Event 1 Day" 24)
- (test-chime-tooltip-day-calculation--create-event-at-hours now "Event 1.5 Days" 36)
- (test-chime-tooltip-day-calculation--create-event-at-hours now "Event 2 Days" 48)
- (test-chime-tooltip-day-calculation--create-event-at-hours now "Event 2.75 Days" 66)))
- (test-file (chime-create-temp-test-file-with-content content))
- (events (with-current-buffer (find-file-noselect test-file)
- (org-mode)
- (goto-char (point-min))
- (let ((evs nil))
- (while (re-search-forward "^\\*+ " nil t)
- (push (chime--gather-info (point-marker)) evs))
- (nreverse evs)))))
- (kill-buffer (get-file-buffer test-file))
-
- (setq chime-modeline-lookahead-minutes 10080)
- (setq chime-tooltip-lookahead-hours 168)
-
- (with-test-time now
- (chime--update-modeline events)
- (let ((tooltip (chime--make-tooltip chime--upcoming-events)))
- ;; Verify each event shows correctly
- (should (string-match-p "Event 1 Day.*(in 1 day)" tooltip))
- (should (string-match-p "Event 1.5 Days.*(in 1 day 12 hours)" tooltip))
- (should (string-match-p "Event 2 Days.*(in 2 days)" tooltip))
- (should (string-match-p "Event 2.75 Days.*(in 2 days 18 hours)" tooltip))
-
- ;; Verify they're all different
- (let ((lines (split-string tooltip "\n")))
- (let ((countdowns (cl-remove-if-not
- (lambda (line) (string-match-p "Event.*day" line))
- lines)))
- ;; Should have 4 distinct countdown lines
- (should (= 4 (length countdowns)))
- ;; All should be unique
- (should (= 4 (length (delete-dups (copy-sequence countdowns))))))))))
- (chime-delete-test-base-dir)))
+ (let* ((now (test-time-today-at 12 0))
+ (content (concat
+ (test-chime-tooltip-day-calculation--create-event-at-hours now "Event 1 Day" 24)
+ (test-chime-tooltip-day-calculation--create-event-at-hours now "Event 1.5 Days" 36)
+ (test-chime-tooltip-day-calculation--create-event-at-hours now "Event 2 Days" 48)
+ (test-chime-tooltip-day-calculation--create-event-at-hours now "Event 2.75 Days" 66))))
+ (test-chime-tooltip-day-calculation--with-tooltip now content
+ ;; Verify each event shows correctly
+ (should (string-match-p "Event 1 Day.*(in 1 day)" tooltip))
+ (should (string-match-p "Event 1.5 Days.*(in 1 day 12 hours)" tooltip))
+ (should (string-match-p "Event 2 Days.*(in 2 days)" tooltip))
+ (should (string-match-p "Event 2.75 Days.*(in 2 days 18 hours)" tooltip))
+
+ ;; Verify they're all different
+ (let ((lines (split-string tooltip "\n")))
+ (let ((countdowns (cl-remove-if-not
+ (lambda (line) (string-match-p "Event.*day" line))
+ lines)))
+ ;; Should have 4 distinct countdown lines
+ (should (= 4 (length countdowns)))
+ ;; All should be unique
+ (should (= 4 (length (delete-dups (copy-sequence countdowns))))))))))
(provide 'test-chime-tooltip-day-calculation)
;;; test-chime-tooltip-day-calculation.el ends here
diff --git a/tests/test-chime-validate-configuration.el b/tests/test-chime-validate-configuration.el
index 8bcc16e..c971632 100644
--- a/tests/test-chime-validate-configuration.el
+++ b/tests/test-chime-validate-configuration.el
@@ -15,7 +15,7 @@
;; External dependencies mocked:
;; - file-exists-p (file I/O)
;; - require (package loading)
-;; - display-warning (UI side effect)
+;; - message (interactive UI side effect)
;;
;; NOT mocked:
;; - Validation logic itself
@@ -238,35 +238,77 @@
;;; Interactive Behavior Tests
-(ert-deftest test-chime-validate-configuration-interactive-calls-display-warning ()
- "Test validation displays warnings when called interactively."
+(ert-deftest test-chime-validate-configuration-interactive-prints-all-checks-with-issues ()
+ "Interactive validation prints ok, warning, error, and summary lines."
(test-chime-validate-configuration-setup)
- (let ((org-agenda-files nil)
- (warning-called nil)
- (chime-enable-modeline t))
- (cl-letf (((symbol-function 'display-warning)
- (lambda (&rest _) (setq warning-called t)))
+ (let ((org-agenda-files '("/exists.org" "/missing.org"))
+ (chime-enable-modeline t)
+ (messages nil))
+ (cl-letf (((symbol-function 'file-exists-p)
+ (lambda (path) (string= path "/exists.org")))
+ ((symbol-function 'require) (lambda (_ &optional _ _) t))
+ ((symbol-function 'boundp)
+ (lambda (sym) (not (eq sym 'global-mode-string))))
+ ((symbol-function 'message)
+ (lambda (format-string &rest args)
+ (push (apply #'format format-string args) messages)))
((symbol-function 'called-interactively-p) (lambda (_) t)))
(chime-validate-configuration)
- (should warning-called)))
+ (setq messages (nreverse messages))
+ (should (member "Chime: Validating configuration..." messages))
+ (should (member "[ok] org-agenda-files is set" messages))
+ (should (cl-some (lambda (msg)
+ (string-match-p
+ "\\[warn\\] org-agenda-files entries exist on disk (2 entries)"
+ msg))
+ messages))
+ (should (member "[ok] org-agenda is loadable" messages))
+ (should (member "[warn] global-mode-string is available" messages))
+ (should (cl-some (lambda (msg)
+ (string-match-p "/missing.org (file)" msg))
+ messages))
+ (should (member "Chime: 0 errors, 2 warnings." messages))))
(test-chime-validate-configuration-teardown))
-(ert-deftest test-chime-validate-configuration-interactive-success-shows-message ()
- "Test validation shows success message when called interactively with valid config."
+(ert-deftest test-chime-validate-configuration-interactive-success-prints-ok-checklist ()
+ "Interactive validation prints every passing check and a zero summary."
(test-chime-validate-configuration-setup)
(let ((org-agenda-files '("/tmp/inbox.org"))
- (message-shown nil)
+ (messages nil)
(chime-enable-modeline t)
(global-mode-string '("")))
(cl-letf (((symbol-function 'file-exists-p) (lambda (_) t))
((symbol-function 'require) (lambda (_ &optional _ _) t))
((symbol-function 'message)
- (lambda (fmt &rest _)
- (when (string-match-p "validation checks passed" fmt)
- (setq message-shown t))))
+ (lambda (format-string &rest args)
+ (push (apply #'format format-string args) messages)))
((symbol-function 'called-interactively-p) (lambda (_) t)))
(chime-validate-configuration)
- (should message-shown)))
+ (setq messages (nreverse messages))
+ (should (member "Chime: Validating configuration..." messages))
+ (should (member "[ok] org-agenda-files is set" messages))
+ (should (member "[ok] org-agenda-files entries exist on disk (1 entries)"
+ messages))
+ (should (member "[ok] org-agenda is loadable" messages))
+ (should (member "[ok] global-mode-string is available" messages))
+ (should (member "Chime: 0 errors, 0 warnings." messages))))
+ (test-chime-validate-configuration-teardown))
+
+(ert-deftest test-chime-validate-configuration-programmatic-shape-unchanged ()
+ "Programmatic validation returns only (SEVERITY MESSAGE) issue pairs."
+ (test-chime-validate-configuration-setup)
+ (let ((org-agenda-files '("/missing.org"))
+ (chime-enable-modeline t)
+ (global-mode-string '("")))
+ (cl-letf (((symbol-function 'file-exists-p) (lambda (_) nil))
+ ((symbol-function 'require) (lambda (_ &optional _ _) t))
+ ((symbol-function 'called-interactively-p) (lambda (_) nil)))
+ (let ((issues (chime-validate-configuration)))
+ (should (= 1 (length issues)))
+ (should (= 2 (length (car issues))))
+ (should (eq :warning (caar issues)))
+ (should (string-match-p "1 org-agenda-files entries don't exist"
+ (cadar issues))))))
(test-chime-validate-configuration-teardown))
(provide 'test-chime-validate-configuration)
diff --git a/tests/test-chime-validation-retry.el b/tests/test-chime-validation-retry.el
index 70188bc..a2765f4 100644
--- a/tests/test-chime-validation-retry.el
+++ b/tests/test-chime-validation-retry.el
@@ -20,37 +20,21 @@
;;; Code:
(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+(require 'testutil-general (expand-file-name "testutil-general.el"))
+(require 'testutil-time (expand-file-name "testutil-time.el"))
+(require 'testutil-events (expand-file-name "testutil-events.el"))
;;; Setup and Teardown
-(defvar test-chime-validation-retry--original-max-retries nil
- "Original value of chime--validation-max-retries for restoration.")
-
-(defvar test-chime-validation-retry--original-agenda-files nil
- "Original value of org-agenda-files for restoration.")
-
-(defun test-chime-validation-retry-setup ()
- "Set up test environment before each test."
- ;; Save original values
- (setq test-chime-validation-retry--original-max-retries chime--validation-max-retries)
- (setq test-chime-validation-retry--original-agenda-files org-agenda-files)
-
- ;; Reset validation state
- (setq chime--validation-done nil)
- (setq chime--validation-retry-count 0)
-
- ;; Set predictable defaults
- (setq chime--validation-max-retries 3))
-
-(defun test-chime-validation-retry-teardown ()
- "Clean up test environment after each test."
- ;; Restore original values
- (setq chime--validation-max-retries test-chime-validation-retry--original-max-retries)
- (setq org-agenda-files test-chime-validation-retry--original-agenda-files)
-
- ;; Reset validation state
- (setq chime--validation-done nil)
- (setq chime--validation-retry-count 0))
+(defmacro test-chime-validation-retry--with-state (&rest body)
+ "Run BODY with isolated validation retry state."
+ (declare (indent 0))
+ `(with-chime-config
+ chime--validation-max-retries 3
+ org-agenda-files nil
+ (let ((chime--validation-done nil)
+ (chime--validation-retry-count 0))
+ ,@body)))
;;; Normal Cases - Retry Behavior
@@ -60,35 +44,32 @@
When org-agenda-files is empty on the first check, chime should show
a friendly waiting message instead of immediately displaying the full
error. This accommodates async org-agenda-files initialization."
- (test-chime-validation-retry-setup)
- (unwind-protect
- (progn
- ;; Empty org-agenda-files to trigger validation failure
- (setq org-agenda-files nil)
-
- ;; Capture message output
- (let ((messages nil))
- (cl-letf (((symbol-function 'message)
- (lambda (format-string &rest args)
- (push (apply #'format format-string args) messages)))
- ;; Mock fetch to prevent actual agenda processing
- ((symbol-function 'chime--fetch-and-process)
- (lambda (callback) nil)))
-
- ;; Call chime-check
- (chime-check)
-
- ;; Should show waiting message
- (should (= chime--validation-retry-count 1))
- (should-not chime--validation-done)
- (should (cl-some (lambda (msg)
- (string-match-p "Waiting for org-agenda-files" msg))
- messages))
- ;; Should NOT show error message
- (should-not (cl-some (lambda (msg)
- (string-match-p "Configuration errors detected" msg))
- messages)))))
- (test-chime-validation-retry-teardown)))
+ (test-chime-validation-retry--with-state
+ ;; Empty org-agenda-files to trigger validation failure
+ (setq org-agenda-files nil)
+
+ ;; Capture message output
+ (let ((messages nil))
+ (cl-letf (((symbol-function 'message)
+ (lambda (format-string &rest args)
+ (push (apply #'format format-string args) messages)))
+ ;; Mock fetch to prevent actual agenda processing
+ ((symbol-function 'chime--fetch-and-process)
+ (lambda (callback) nil)))
+
+ ;; Call chime-check
+ (chime-check)
+
+ ;; Should show waiting message
+ (should (= chime--validation-retry-count 1))
+ (should-not chime--validation-done)
+ (should (cl-some (lambda (msg)
+ (string-match-p "Waiting for org-agenda-files" msg))
+ messages))
+ ;; Should NOT show error message
+ (should-not (cl-some (lambda (msg)
+ (string-match-p "Configuration errors detected" msg))
+ messages))))))
(ert-deftest test-chime-validation-retry-normal-success-resets-counter ()
"Test successful validation after retry resets counter to zero.
@@ -96,83 +77,74 @@ error. This accommodates async org-agenda-files initialization."
When validation succeeds on a retry attempt, the retry counter should
be reset to 0, allowing fresh retry attempts if validation fails again
later (e.g., after mode restart)."
- (test-chime-validation-retry-setup)
- (unwind-protect
- (progn
- ;; Simulate one failed attempt
- (setq chime--validation-retry-count 1)
+ (test-chime-validation-retry--with-state
+ ;; Simulate one failed attempt
+ (setq chime--validation-retry-count 1)
- ;; Set valid org-agenda-files
- (setq org-agenda-files '("/tmp/test.org"))
+ ;; Set valid org-agenda-files
+ (setq org-agenda-files '("/tmp/test.org"))
- ;; Mock fetch to prevent actual agenda processing
- (cl-letf (((symbol-function 'chime--fetch-and-process)
- (lambda (callback) nil)))
+ ;; Mock fetch to prevent actual agenda processing
+ (cl-letf (((symbol-function 'chime--fetch-and-process)
+ (lambda (callback) nil)))
- ;; Call chime-check - should succeed
- (chime-check)
+ ;; Call chime-check - should succeed
+ (chime-check)
- ;; Counter should be reset
- (should (= chime--validation-retry-count 0))
- ;; Validation marked as done
- (should chime--validation-done)))
- (test-chime-validation-retry-teardown)))
+ ;; Counter should be reset
+ (should (= chime--validation-retry-count 0))
+ ;; Validation marked as done
+ (should chime--validation-done))))
(ert-deftest test-chime-validation-retry-normal-multiple-retries-increment ()
"Test multiple validation failures increment counter correctly.
Each validation failure should increment the retry counter by 1,
allowing the system to track how many retries have been attempted."
- (test-chime-validation-retry-setup)
- (unwind-protect
- (progn
- ;; Empty org-agenda-files
- (setq org-agenda-files nil)
-
- ;; Mock fetch
- (cl-letf (((symbol-function 'chime--fetch-and-process)
- (lambda (callback) nil))
- ((symbol-function 'message)
- (lambda (&rest args) nil)))
-
- ;; First attempt
- (chime-check)
- (should (= chime--validation-retry-count 1))
-
- ;; Second attempt
- (chime-check)
- (should (= chime--validation-retry-count 2))
-
- ;; Third attempt
- (chime-check)
- (should (= chime--validation-retry-count 3))))
- (test-chime-validation-retry-teardown)))
+ (test-chime-validation-retry--with-state
+ ;; Empty org-agenda-files
+ (setq org-agenda-files nil)
+
+ ;; Mock fetch
+ (cl-letf (((symbol-function 'chime--fetch-and-process)
+ (lambda (callback) nil))
+ ((symbol-function 'message)
+ (lambda (&rest args) nil)))
+
+ ;; First attempt
+ (chime-check)
+ (should (= chime--validation-retry-count 1))
+
+ ;; Second attempt
+ (chime-check)
+ (should (= chime--validation-retry-count 2))
+
+ ;; Third attempt
+ (chime-check)
+ (should (= chime--validation-retry-count 3)))))
(ert-deftest test-chime-validation-retry-normal-successful-validation-proceeds ()
"Test successful validation proceeds with event checking.
When validation passes, chime-check should proceed to fetch and
process events normally."
- (test-chime-validation-retry-setup)
- (unwind-protect
- (progn
- ;; Valid org-agenda-files
- (setq org-agenda-files '("/tmp/test.org"))
-
- ;; Track if fetch was called
- (let ((fetch-called nil))
- (cl-letf (((symbol-function 'chime--fetch-and-process)
- (lambda (callback)
- (setq fetch-called t))))
-
- ;; Call chime-check
- (chime-check)
-
- ;; Should proceed to fetch
- (should fetch-called)
- (should chime--validation-done)
- (should (= chime--validation-retry-count 0)))))
- (test-chime-validation-retry-teardown)))
+ (test-chime-validation-retry--with-state
+ ;; Valid org-agenda-files
+ (setq org-agenda-files '("/tmp/test.org"))
+
+ ;; Track if fetch was called
+ (let ((fetch-called nil))
+ (cl-letf (((symbol-function 'chime--fetch-and-process)
+ (lambda (callback)
+ (setq fetch-called t))))
+
+ ;; Call chime-check
+ (chime-check)
+
+ ;; Should proceed to fetch
+ (should fetch-called)
+ (should chime--validation-done)
+ (should (= chime--validation-retry-count 0))))))
;;; Boundary Cases - Edge Conditions
@@ -182,159 +154,144 @@ process events normally."
When chime--validation-max-retries is set to 0, validation failures
should immediately show the full error message without any retry
attempts."
- (test-chime-validation-retry-setup)
- (unwind-protect
- (progn
- ;; Set max retries to 0
- (setq chime--validation-max-retries 0)
-
- ;; Empty org-agenda-files
- (setq org-agenda-files nil)
-
- ;; Capture message output
- (let ((messages nil))
- (cl-letf (((symbol-function 'message)
- (lambda (format-string &rest args)
- (push (apply #'format format-string args) messages)))
- ((symbol-function 'chime--fetch-and-process)
- (lambda (callback) nil)))
-
- ;; Call chime-check
- (chime-check)
-
- ;; Counter incremented
- (should (= chime--validation-retry-count 1))
- ;; Should show error, not waiting message
- (should (cl-some (lambda (msg)
- (string-match-p "Configuration errors detected" msg))
- messages))
- (should-not (cl-some (lambda (msg)
- (string-match-p "Waiting for" msg))
- messages)))))
- (test-chime-validation-retry-teardown)))
+ (test-chime-validation-retry--with-state
+ ;; Set max retries to 0
+ (setq chime--validation-max-retries 0)
+
+ ;; Empty org-agenda-files
+ (setq org-agenda-files nil)
+
+ ;; Capture message output
+ (let ((messages nil))
+ (cl-letf (((symbol-function 'message)
+ (lambda (format-string &rest args)
+ (push (apply #'format format-string args) messages)))
+ ((symbol-function 'chime--fetch-and-process)
+ (lambda (callback) nil)))
+
+ ;; Call chime-check
+ (chime-check)
+
+ ;; Counter incremented
+ (should (= chime--validation-retry-count 1))
+ ;; Should show error, not waiting message
+ (should (cl-some (lambda (msg)
+ (string-match-p "Configuration errors detected" msg))
+ messages))
+ (should-not (cl-some (lambda (msg)
+ (string-match-p "Waiting for" msg))
+ messages))))))
(ert-deftest test-chime-validation-retry-boundary-max-retries-one ()
"Test max-retries=1 allows one retry before showing error.
First attempt should show waiting message, second attempt should
show full error."
- (test-chime-validation-retry-setup)
- (unwind-protect
- (progn
- ;; Set max retries to 1
- (setq chime--validation-max-retries 1)
-
- ;; Empty org-agenda-files
- (setq org-agenda-files nil)
-
- (cl-letf (((symbol-function 'chime--fetch-and-process)
- (lambda (callback) nil)))
-
- ;; First attempt - should show waiting
- (let ((messages nil))
- (cl-letf (((symbol-function 'message)
- (lambda (format-string &rest args)
- (push (apply #'format format-string args) messages))))
- (chime-check)
- (should (= chime--validation-retry-count 1))
- (should (cl-some (lambda (msg)
- (string-match-p "Waiting for" msg))
- messages))))
-
- ;; Second attempt - should show error
- (let ((messages nil))
- (cl-letf (((symbol-function 'message)
- (lambda (format-string &rest args)
- (push (apply #'format format-string args) messages))))
- (chime-check)
- (should (= chime--validation-retry-count 2))
- (should (cl-some (lambda (msg)
- (string-match-p "Configuration errors detected" msg))
- messages))))))
- (test-chime-validation-retry-teardown)))
+ (test-chime-validation-retry--with-state
+ ;; Set max retries to 1
+ (setq chime--validation-max-retries 1)
+
+ ;; Empty org-agenda-files
+ (setq org-agenda-files nil)
+
+ (cl-letf (((symbol-function 'chime--fetch-and-process)
+ (lambda (callback) nil)))
+
+ ;; First attempt - should show waiting
+ (let ((messages nil))
+ (cl-letf (((symbol-function 'message)
+ (lambda (format-string &rest args)
+ (push (apply #'format format-string args) messages))))
+ (chime-check)
+ (should (= chime--validation-retry-count 1))
+ (should (cl-some (lambda (msg)
+ (string-match-p "Waiting for" msg))
+ messages))))
+
+ ;; Second attempt - should show error
+ (let ((messages nil))
+ (cl-letf (((symbol-function 'message)
+ (lambda (format-string &rest args)
+ (push (apply #'format format-string args) messages))))
+ (chime-check)
+ (should (= chime--validation-retry-count 2))
+ (should (cl-some (lambda (msg)
+ (string-match-p "Configuration errors detected" msg))
+ messages)))))))
(ert-deftest test-chime-validation-retry-boundary-exactly-at-threshold ()
"Test behavior exactly at max-retries threshold.
The (retry_count + 1)th attempt should show the error message."
- (test-chime-validation-retry-setup)
- (unwind-protect
- (progn
- ;; Default max retries = 3
- (setq chime--validation-max-retries 3)
- (setq org-agenda-files nil)
-
- (cl-letf (((symbol-function 'chime--fetch-and-process)
- (lambda (callback) nil)))
-
- ;; Attempts 1-3: waiting messages
- (dotimes (_ 3)
- (let ((messages nil))
- (cl-letf (((symbol-function 'message)
- (lambda (format-string &rest args)
- (push (apply #'format format-string args) messages))))
- (chime-check)
- (should (cl-some (lambda (msg)
- (string-match-p "Waiting for" msg))
- messages)))))
-
- ;; Attempt 4: should show error
- (let ((messages nil))
- (cl-letf (((symbol-function 'message)
- (lambda (format-string &rest args)
- (push (apply #'format format-string args) messages))))
- (chime-check)
- (should (= chime--validation-retry-count 4))
- (should (cl-some (lambda (msg)
- (string-match-p "Configuration errors detected" msg))
- messages))))))
- (test-chime-validation-retry-teardown)))
+ (test-chime-validation-retry--with-state
+ ;; Default max retries = 3
+ (setq chime--validation-max-retries 3)
+ (setq org-agenda-files nil)
+
+ (cl-letf (((symbol-function 'chime--fetch-and-process)
+ (lambda (callback) nil)))
+
+ ;; Attempts 1-3: waiting messages
+ (dotimes (_ 3)
+ (let ((messages nil))
+ (cl-letf (((symbol-function 'message)
+ (lambda (format-string &rest args)
+ (push (apply #'format format-string args) messages))))
+ (chime-check)
+ (should (cl-some (lambda (msg)
+ (string-match-p "Waiting for" msg))
+ messages)))))
+
+ ;; Attempt 4: should show error
+ (let ((messages nil))
+ (cl-letf (((symbol-function 'message)
+ (lambda (format-string &rest args)
+ (push (apply #'format format-string args) messages))))
+ (chime-check)
+ (should (= chime--validation-retry-count 4))
+ (should (cl-some (lambda (msg)
+ (string-match-p "Configuration errors detected" msg))
+ messages)))))))
(ert-deftest test-chime-validation-retry-boundary-stop-resets-counter ()
"Test chime--stop resets retry counter to zero.
When chime-mode is stopped, the retry counter should be reset to
allow fresh retry attempts on next start."
- (test-chime-validation-retry-setup)
- (unwind-protect
- (progn
- ;; Simulate some failed attempts
- (setq chime--validation-retry-count 5)
- (setq chime--validation-done nil)
+ (test-chime-validation-retry--with-state
+ ;; Simulate some failed attempts
+ (setq chime--validation-retry-count 5)
+ (setq chime--validation-done nil)
- ;; Call stop
- (chime--stop)
+ ;; Call stop
+ (chime--stop)
- ;; Counter should be reset
- (should (= chime--validation-retry-count 0))
- (should-not chime--validation-done))
- (test-chime-validation-retry-teardown)))
+ ;; Counter should be reset
+ (should (= chime--validation-retry-count 0))
+ (should-not chime--validation-done)))
(ert-deftest test-chime-validation-retry-boundary-empty-agenda-files ()
"Test empty org-agenda-files list triggers retry.
An empty list should be treated the same as nil - both should
trigger validation failure and retry."
- (test-chime-validation-retry-setup)
- (unwind-protect
- (progn
- ;; Empty list (not nil)
- (setq org-agenda-files '())
-
- (let ((messages nil))
- (cl-letf (((symbol-function 'message)
- (lambda (format-string &rest args)
- (push (apply #'format format-string args) messages)))
- ((symbol-function 'chime--fetch-and-process)
- (lambda (callback) nil)))
-
- ;; Should trigger retry
- (chime-check)
- (should (= chime--validation-retry-count 1))
- (should (cl-some (lambda (msg)
- (string-match-p "Waiting for" msg))
- messages)))))
- (test-chime-validation-retry-teardown)))
+ (test-chime-validation-retry--with-state
+ ;; Empty list (not nil)
+ (setq org-agenda-files '())
+
+ (let ((messages nil))
+ (cl-letf (((symbol-function 'message)
+ (lambda (format-string &rest args)
+ (push (apply #'format format-string args) messages)))
+ ((symbol-function 'chime--fetch-and-process)
+ (lambda (callback) nil)))
+
+ ;; Should trigger retry
+ (chime-check)
+ (should (= chime--validation-retry-count 1))
+ (should (cl-some (lambda (msg)
+ (string-match-p "Waiting for" msg))
+ messages))))))
;;; Error Cases - Failure Scenarios
@@ -343,85 +300,76 @@ trigger validation failure and retry."
After max retries exceeded, the full validation error should be
displayed with all error details in the *Messages* buffer."
- (test-chime-validation-retry-setup)
- (unwind-protect
- (progn
- (setq chime--validation-max-retries 2)
- (setq org-agenda-files nil)
-
- (cl-letf (((symbol-function 'chime--fetch-and-process)
- (lambda (callback) nil)))
-
- ;; Exhaust retries
- (dotimes (_ 3)
- (let ((messages nil))
- (cl-letf (((symbol-function 'message)
- (lambda (format-string &rest args)
- (push (apply #'format format-string args) messages))))
- (chime-check))))
-
- ;; Verify error message on next attempt
- (let ((messages nil))
- (cl-letf (((symbol-function 'message)
- (lambda (format-string &rest args)
- (push (apply #'format format-string args) messages))))
- (chime-check)
- ;; Should show error message (detailed error with retry count goes to *Messages* buffer via chime--log-silently)
- (should (cl-some (lambda (msg)
- (string-match-p "Configuration errors detected" msg))
- messages))))))
- (test-chime-validation-retry-teardown)))
+ (test-chime-validation-retry--with-state
+ (setq chime--validation-max-retries 2)
+ (setq org-agenda-files nil)
+
+ (cl-letf (((symbol-function 'chime--fetch-and-process)
+ (lambda (callback) nil)))
+
+ ;; Exhaust retries
+ (dotimes (_ 3)
+ (let ((messages nil))
+ (cl-letf (((symbol-function 'message)
+ (lambda (format-string &rest args)
+ (push (apply #'format format-string args) messages))))
+ (chime-check))))
+
+ ;; Verify error message on next attempt
+ (let ((messages nil))
+ (cl-letf (((symbol-function 'message)
+ (lambda (format-string &rest args)
+ (push (apply #'format format-string args) messages))))
+ (chime-check)
+ ;; Should show error message (detailed error with retry count goes to *Messages* buffer via chime--log-silently)
+ (should (cl-some (lambda (msg)
+ (string-match-p "Configuration errors detected" msg))
+ messages)))))))
(ert-deftest test-chime-validation-retry-error-persistent-failure ()
"Test validation failure persisting through all retries.
If org-agenda-files remains empty through all retry attempts,
validation should never be marked as done."
- (test-chime-validation-retry-setup)
- (unwind-protect
- (progn
- (setq chime--validation-max-retries 3)
- (setq org-agenda-files nil)
-
- (cl-letf (((symbol-function 'chime--fetch-and-process)
- (lambda (callback) nil))
- ((symbol-function 'message)
- (lambda (&rest args) nil)))
-
- ;; Multiple attempts, all failing
- (dotimes (_ 10)
- (chime-check)
- ;; Should never mark as done
- (should-not chime--validation-done))
-
- ;; Counter keeps incrementing
- (should (= chime--validation-retry-count 10))))
- (test-chime-validation-retry-teardown)))
+ (test-chime-validation-retry--with-state
+ (setq chime--validation-max-retries 3)
+ (setq org-agenda-files nil)
+
+ (cl-letf (((symbol-function 'chime--fetch-and-process)
+ (lambda (callback) nil))
+ ((symbol-function 'message)
+ (lambda (&rest args) nil)))
+
+ ;; Multiple attempts, all failing
+ (dotimes (_ 10)
+ (chime-check)
+ ;; Should never mark as done
+ (should-not chime--validation-done))
+
+ ;; Counter keeps incrementing
+ (should (= chime--validation-retry-count 10)))))
(ert-deftest test-chime-validation-retry-error-counter-large-value ()
"Test retry counter handles large values without overflow.
The retry counter should continue incrementing correctly even with
many retry attempts, ensuring no integer overflow issues."
- (test-chime-validation-retry-setup)
- (unwind-protect
- (progn
- (setq chime--validation-max-retries 1000)
- (setq org-agenda-files nil)
-
- (cl-letf (((symbol-function 'chime--fetch-and-process)
- (lambda (callback) nil))
- ((symbol-function 'message)
- (lambda (&rest args) nil)))
-
- ;; Many attempts
- (dotimes (i 100)
- (chime-check)
- (should (= chime--validation-retry-count (1+ i))))
-
- ;; Should still be counting correctly
- (should (= chime--validation-retry-count 100))))
- (test-chime-validation-retry-teardown)))
+ (test-chime-validation-retry--with-state
+ (setq chime--validation-max-retries 1000)
+ (setq org-agenda-files nil)
+
+ (cl-letf (((symbol-function 'chime--fetch-and-process)
+ (lambda (callback) nil))
+ ((symbol-function 'message)
+ (lambda (&rest args) nil)))
+
+ ;; Many attempts
+ (dotimes (i 100)
+ (chime-check)
+ (should (= chime--validation-retry-count (1+ i))))
+
+ ;; Should still be counting correctly
+ (should (= chime--validation-retry-count 100)))))
(provide 'test-chime-validation-retry)
;;; test-chime-validation-retry.el ends here
diff --git a/tests/testutil-events.el b/tests/testutil-events.el
index 0ee3d57..91fdcfc 100644
--- a/tests/testutil-events.el
+++ b/tests/testutil-events.el
@@ -128,9 +128,9 @@ Example:
(list (cons ts-str time))
'((10 . medium)))))
(should (string= \"Meeting\" (cdr (assoc 'title event)))))"
- `((times . ,time-alist)
- (title . ,title)
- (intervals . ,(or intervals '((10 . medium))))))
+ (chime--make-event time-alist
+ title
+ (or intervals '((10 . medium)))))
(defun test-make-simple-event (title time &optional interval-minutes severity)
"Create simple event data structure with single time and interval.
@@ -182,6 +182,19 @@ Example:
`(let ((,events-var (test-gather-events-from-content ,content)))
,@body))
+(defmacro with-chime-tooltip-from-content (content tooltip-var &rest body)
+ "Create org CONTENT, update modeline events, and bind TOOLTIP-VAR.
+The helper keeps modeline globals dynamically isolated so tests can assert on
+tooltip text without leaking `chime--upcoming-events' or `chime-modeline-string'."
+ (declare (indent 2))
+ (let ((events-var (make-symbol "events")))
+ `(let ((chime--upcoming-events nil)
+ (chime-modeline-string nil))
+ (with-gathered-events ,content ,events-var
+ (chime--update-modeline ,events-var)
+ (let ((,tooltip-var (chime--make-tooltip chime--upcoming-events)))
+ ,@body)))))
+
;;; Setup/Teardown Helpers
(defun test-standard-setup ()