diff options
| author | Craig Jennings <c@cjennings.net> | 2026-04-04 12:40:27 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-04-04 12:40:27 -0500 |
| commit | 69c8deb2d7821c78475dcbb08458df6b0e56d559 (patch) | |
| tree | a55c1d8b3fc723fc25bf5ca47f668902a47bf4ed | |
| parent | fe5a31072bfa2f6451769008a63ad5b0d9a3de17 (diff) | |
| download | chime-69c8deb2d7821c78475dcbb08458df6b0e56d559.tar.gz chime-69c8deb2d7821c78475dcbb08458df6b0e56d559.zip | |
Add 94 tests across 10 new test files and fix 2 bugs
New test coverage for previously untested functions:
- filter-day-wide-events, time utilities, day-wide time matching
- make-tooltip, event-is-today, get-tags, done-keywords-predicate
- extract-title, propertize-modeline-string, warn-persistent-failures
- log-silently
Bug fixes discovered by new tests:
- Fix pluralization in no-events tooltip ("1 hours" -> "1 hour", etc.)
- Fix chime--get-tags returning ("") instead of nil for untagged headings
| -rw-r--r-- | chime.el | 15 | ||||
| -rw-r--r-- | tests/test-chime-day-wide-time-matching.el | 150 | ||||
| -rw-r--r-- | tests/test-chime-event-is-today.el | 139 | ||||
| -rw-r--r-- | tests/test-chime-extract-title.el | 127 | ||||
| -rw-r--r-- | tests/test-chime-filter-day-wide-events.el | 136 | ||||
| -rw-r--r-- | tests/test-chime-get-tags.el | 158 | ||||
| -rw-r--r-- | tests/test-chime-log-silently.el | 88 | ||||
| -rw-r--r-- | tests/test-chime-make-tooltip.el | 245 | ||||
| -rw-r--r-- | tests/test-chime-propertize-modeline.el | 163 | ||||
| -rw-r--r-- | tests/test-chime-time-utilities.el | 174 | ||||
| -rw-r--r-- | tests/test-chime-warn-persistent-failures.el | 165 |
11 files changed, 1553 insertions, 7 deletions
@@ -1067,10 +1067,12 @@ Returns an alist of (DATE-STRING . EVENTS-LIST)." (let* ((hours (/ lookahead-minutes 60)) (days (/ hours 24)) (timeframe (cond - ((>= days 7) (format "%d days" days)) - ((>= hours 24) (format "%.1f days" (/ hours 24.0))) - ((>= hours 1) (format "%d hours" hours)) - (t (format "%d minutes" lookahead-minutes)))) + ((>= days 7) (format "%d day%s" days (if (= days 1) "" "s"))) + ((>= hours 24) (let ((d (/ hours 24.0))) + (format "%.1f day%s" d (if (= d 1.0) "" "s")))) + ((>= hours 1) (format "%d hour%s" hours (if (= hours 1) "" "s"))) + (t (format "%d minute%s" lookahead-minutes + (if (= lookahead-minutes 1) "" "s"))))) (header (format-time-string chime-tooltip-header-format)) (increase-var "chime-tooltip-lookahead-hours")) (concat header "\n" @@ -1240,9 +1242,8 @@ Tooltip shows events within `chime-tooltip-lookahead-hours' hours." (defun chime--get-tags (marker) "Retrieve tags of MARKER." - (-> (org-entry-get marker "TAGS") - (or "") - (org-split-string ":"))) + (when-let* ((tags-str (org-entry-get marker "TAGS"))) + (org-split-string tags-str ":"))) (defun chime--whitelist-predicates () "Return list of whitelist predicate functions. diff --git a/tests/test-chime-day-wide-time-matching.el b/tests/test-chime-day-wide-time-matching.el new file mode 100644 index 0000000..0e92093 --- /dev/null +++ b/tests/test-chime-day-wide-time-matching.el @@ -0,0 +1,150 @@ +;;; test-chime-day-wide-time-matching.el --- Tests for day-wide time matching -*- 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 day-wide time matching functions: +;; - chime-current-time-matches-time-of-day-string +;; - chime-current-time-is-day-wide-time +;; +;; These functions determine when to fire notifications for all-day events +;; by comparing the current time to configured alert times like "08:00". +;; +;; IMPORTANT: Mock times must be computed BEFORE entering with-test-time, +;; because test-time-today-at calls current-time internally. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.el")) +(require 'testutil-time (expand-file-name "testutil-time.el")) + +;;;; Tests for chime-current-time-matches-time-of-day-string + +;;; Normal Cases + +(ert-deftest test-chime-time-matches-string-exact-match () + "Should return truthy when current time matches the time-of-day string." + (let ((mock-time (test-time-today-at 8 0))) + (with-test-time mock-time + (should (chime-current-time-matches-time-of-day-string "8:00"))))) + +(ert-deftest test-chime-time-matches-string-no-match () + "Should return nil when current time does not match." + (let ((mock-time (test-time-today-at 9 0))) + (with-test-time mock-time + (should-not (chime-current-time-matches-time-of-day-string "8:00"))))) + +(ert-deftest test-chime-time-matches-string-afternoon () + "Should match afternoon times correctly." + (let ((mock-time (test-time-today-at 17 0))) + (with-test-time mock-time + (should (chime-current-time-matches-time-of-day-string "17:00"))))) + +;;; Boundary Cases + +(ert-deftest test-chime-time-matches-string-midnight () + "Should match midnight (00:00)." + (let ((mock-time (test-time-today-at 0 0))) + (with-test-time mock-time + (should (chime-current-time-matches-time-of-day-string "0:00"))))) + +(ert-deftest test-chime-time-matches-string-end-of-day () + "Should match 23:59." + (let ((mock-time (test-time-today-at 23 59))) + (with-test-time mock-time + (should (chime-current-time-matches-time-of-day-string "23:59"))))) + +(ert-deftest test-chime-time-matches-string-off-by-one-minute () + "One minute off should not match." + (let ((mock-time (test-time-today-at 8 1))) + (with-test-time mock-time + (should-not (chime-current-time-matches-time-of-day-string "8:00"))))) + +(ert-deftest test-chime-time-matches-string-leading-zero () + "Should match with leading zero in time string (08:00)." + (let ((mock-time (test-time-today-at 8 0))) + (with-test-time mock-time + (should (chime-current-time-matches-time-of-day-string "08:00"))))) + +;;;; Tests for chime-current-time-is-day-wide-time + +;;; Normal Cases + +(ert-deftest test-chime-is-day-wide-time-matches-single-entry () + "Should return truthy when current time matches the configured alert time." + (let ((mock-time (test-time-today-at 8 0))) + (with-test-time mock-time + (let ((chime-day-wide-alert-times '("08:00"))) + (should (chime-current-time-is-day-wide-time)))))) + +(ert-deftest test-chime-is-day-wide-time-matches-second-entry () + "Should return truthy when current time matches any entry, not just first." + (let ((mock-time (test-time-today-at 17 0))) + (with-test-time mock-time + (let ((chime-day-wide-alert-times '("08:00" "17:00"))) + (should (chime-current-time-is-day-wide-time)))))) + +(ert-deftest test-chime-is-day-wide-time-no-match () + "Should return nil when current time matches no configured alert times." + (let ((mock-time (test-time-today-at 12 0))) + (with-test-time mock-time + (let ((chime-day-wide-alert-times '("08:00" "17:00"))) + (should-not (chime-current-time-is-day-wide-time)))))) + +;;; Boundary Cases + +(ert-deftest test-chime-is-day-wide-time-empty-list () + "Should return nil when alert times list is empty." + (let ((mock-time (test-time-today-at 8 0))) + (with-test-time mock-time + (let ((chime-day-wide-alert-times '())) + (should-not (chime-current-time-is-day-wide-time)))))) + +(ert-deftest test-chime-is-day-wide-time-nil-list () + "Should return nil when alert times list is nil." + (let ((mock-time (test-time-today-at 8 0))) + (with-test-time mock-time + (let ((chime-day-wide-alert-times nil)) + (should-not (chime-current-time-is-day-wide-time)))))) + +(ert-deftest test-chime-is-day-wide-time-matches-first-of-many () + "Should return truthy when matching the first of several alert times." + (let ((mock-time (test-time-today-at 8 0))) + (with-test-time mock-time + (let ((chime-day-wide-alert-times '("08:00" "12:00" "17:00"))) + (should (chime-current-time-is-day-wide-time)))))) + +(provide 'test-chime-day-wide-time-matching) +;;; test-chime-day-wide-time-matching.el ends here diff --git a/tests/test-chime-event-is-today.el b/tests/test-chime-event-is-today.el new file mode 100644 index 0000000..112512b --- /dev/null +++ b/tests/test-chime-event-is-today.el @@ -0,0 +1,139 @@ +;;; test-chime-event-is-today.el --- Tests for chime-event-is-today -*- 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-event-is-today function. +;; This function checks if an event has any timestamps specifically on today's +;; date (not past days, not future days). +;; +;; NOTE: These tests use real dates (not with-test-time) because +;; chime-event-is-today uses (decode-time) without arguments internally, +;; which calls the C-level current_time and bypasses Lisp-level mocking. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.el")) +(require 'testutil-time (expand-file-name "testutil-time.el")) + +;;; Helpers — build events at real dates + +(defun test--real-today-at (hour minute) + "Return Emacs time for the real today at HOUR:MINUTE." + (let ((d (decode-time (current-time)))) + (encode-time 0 minute hour + (decoded-time-day d) + (decoded-time-month d) + (decoded-time-year d)))) + +(defun test--real-yesterday-at (hour minute) + "Return Emacs time for the real yesterday at HOUR:MINUTE." + (let ((d (decode-time (time-subtract (current-time) (days-to-time 1))))) + (encode-time 0 minute hour + (decoded-time-day d) + (decoded-time-month d) + (decoded-time-year d)))) + +(defun test--real-tomorrow-at (hour minute) + "Return Emacs time for the real tomorrow at HOUR:MINUTE." + (let ((d (decode-time (time-add (current-time) (days-to-time 1))))) + (encode-time 0 minute hour + (decoded-time-day d) + (decoded-time-month d) + (decoded-time-year d)))) + +(defun test--make-timed-event (time) + "Make an event alist with a single timed timestamp at TIME." + (let ((ts (test-timestamp-string time))) + `((times . ((,ts . ,time)))))) + +(defun test--make-all-day-event (time) + "Make an event alist with a single all-day timestamp at TIME." + (let ((ts (test-timestamp-string time t))) + `((times . ((,ts . nil)))))) + +;;; Normal Cases + +(ert-deftest test-chime-event-is-today-timed-event-today () + "A timed event happening today should return truthy." + (let ((event (test--make-timed-event (test--real-today-at 14 30)))) + (should (chime-event-is-today event)))) + +(ert-deftest test-chime-event-is-today-all-day-event-today () + "An all-day event for today should return truthy." + (let ((event (test--make-all-day-event (test--real-today-at 0 0)))) + (should (chime-event-is-today event)))) + +(ert-deftest test-chime-event-is-today-yesterday-returns-nil () + "An event from yesterday should return nil." + (let ((event (test--make-timed-event (test--real-yesterday-at 14 30)))) + (should-not (chime-event-is-today event)))) + +(ert-deftest test-chime-event-is-today-tomorrow-returns-nil () + "An event for tomorrow should return nil." + (let ((event (test--make-timed-event (test--real-tomorrow-at 14 30)))) + (should-not (chime-event-is-today event)))) + +(ert-deftest test-chime-event-is-today-past-timed-event-today () + "A timed event earlier today (in the past) should return truthy." + (let ((event (test--make-timed-event (test--real-today-at 0 1)))) + (should (chime-event-is-today event)))) + +(ert-deftest test-chime-event-is-today-future-timed-event-today () + "A timed event later today (in the future) should return truthy." + (let ((event (test--make-timed-event (test--real-today-at 23 58)))) + (should (chime-event-is-today event)))) + +;;; Boundary Cases + +(ert-deftest test-chime-event-is-today-event-at-2359-today () + "An event at 23:59 today should return truthy." + (let ((event (test--make-timed-event (test--real-today-at 23 59)))) + (should (chime-event-is-today event)))) + +(ert-deftest test-chime-event-is-today-event-at-0000-today () + "An event at 00:00 today should return truthy." + (let ((event (test--make-timed-event (test--real-today-at 0 0)))) + (should (chime-event-is-today event)))) + +;;; Error Cases + +(ert-deftest test-chime-event-is-today-empty-times-returns-nil () + "An event with no times should return nil." + (let ((event '((times . ())))) + (should-not (chime-event-is-today event)))) + +(provide 'test-chime-event-is-today) +;;; test-chime-event-is-today.el ends here diff --git a/tests/test-chime-extract-title.el b/tests/test-chime-extract-title.el new file mode 100644 index 0000000..ad100e0 --- /dev/null +++ b/tests/test-chime-extract-title.el @@ -0,0 +1,127 @@ +;;; test-chime-extract-title.el --- Tests for chime--extract-title -*- 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--extract-title function. +;; This function extracts the title from an org heading at a marker, +;; stripping TODO keywords, tags, and priority, then sanitizing the result. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.el")) + +;;; Normal Cases + +(ert-deftest test-chime-extract-title-plain-heading () + "Plain heading should return just the title." + (with-temp-buffer + (org-mode) + (insert "* Team Meeting\n") + (goto-char (point-min)) + (should (string= "Team Meeting" (chime--extract-title (point-marker)))))) + +(ert-deftest test-chime-extract-title-todo-heading () + "TODO heading should strip the keyword, returning only the title." + (with-temp-buffer + (org-mode) + (insert "* TODO Review PR\n") + (goto-char (point-min)) + (should (string= "Review PR" (chime--extract-title (point-marker)))))) + +(ert-deftest test-chime-extract-title-heading-with-tags () + "Heading with tags should strip the tags." + (with-temp-buffer + (org-mode) + (insert "* Team Meeting :work:\n") + (goto-char (point-min)) + (should (string= "Team Meeting" (chime--extract-title (point-marker)))))) + +(ert-deftest test-chime-extract-title-heading-with-priority () + "Heading with priority should strip the priority cookie." + (with-temp-buffer + (org-mode) + (insert "* TODO [#A] Urgent task\n") + (goto-char (point-min)) + (should (string= "Urgent task" (chime--extract-title (point-marker)))))) + +(ert-deftest test-chime-extract-title-unicode () + "Heading with unicode characters should be preserved." + (with-temp-buffer + (org-mode) + (insert "* Café meeting with André\n") + (goto-char (point-min)) + (should (string= "Café meeting with André" (chime--extract-title (point-marker)))))) + +;;; Boundary Cases + +(ert-deftest test-chime-extract-title-todo-only-heading () + "Heading with only a TODO keyword and no title text." + (with-temp-buffer + (org-mode) + (insert "* TODO\n") + (goto-char (point-min)) + ;; Should return empty string (nil title is sanitized to "") + (should (stringp (chime--extract-title (point-marker)))))) + +;;; Error Cases - Sanitization + +(ert-deftest test-chime-extract-title-unmatched-paren () + "Title with unmatched parenthesis should be sanitized." + (with-temp-buffer + (org-mode) + (insert "* Meeting (rescheduled\n") + (goto-char (point-min)) + (let ((title (chime--extract-title (point-marker)))) + ;; Sanitizer should balance the paren + (should (stringp title)) + (should (string-match-p "rescheduled" title)) + ;; Should have balanced parens + (should (= (cl-count ?\( title) + (cl-count ?\) title)))))) + +(ert-deftest test-chime-extract-title-unmatched-bracket () + "Title with unmatched bracket should be sanitized." + (with-temp-buffer + (org-mode) + (insert "* Review [draft\n") + (goto-char (point-min)) + (let ((title (chime--extract-title (point-marker)))) + (should (stringp title)) + (should (= (cl-count ?\[ title) + (cl-count ?\] title)))))) + +(provide 'test-chime-extract-title) +;;; test-chime-extract-title.el ends here diff --git a/tests/test-chime-filter-day-wide-events.el b/tests/test-chime-filter-day-wide-events.el new file mode 100644 index 0000000..885a10d --- /dev/null +++ b/tests/test-chime-filter-day-wide-events.el @@ -0,0 +1,136 @@ +;;; test-chime-filter-day-wide-events.el --- Tests for chime--filter-day-wide-events -*- 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--filter-day-wide-events function. +;; This function filters a times alist to keep only entries that have +;; a time component in their timestamp string (i.e., timed events), +;; removing all-day events. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.el")) +(require 'testutil-time (expand-file-name "testutil-time.el")) + +;;; Normal Cases + +(ert-deftest test-chime-filter-day-wide-events-keeps-timed-event () + "Timed events (with HH:MM) should be kept." + (let* ((time (test-time-tomorrow-at 14 30)) + (ts (test-timestamp-string time)) + (times (list (cons ts time))) + (result (chime--filter-day-wide-events times))) + (should (= 1 (length result))) + (should (equal (car (car result)) ts)))) + +(ert-deftest test-chime-filter-day-wide-events-removes-all-day-event () + "All-day events (no HH:MM) should be removed." + (let* ((time (test-time-tomorrow-at 0 0)) + (ts (test-timestamp-string time t)) ;; all-day = t + (times (list (cons ts time))) + (result (chime--filter-day-wide-events times))) + (should (= 0 (length result))))) + +(ert-deftest test-chime-filter-day-wide-events-mixed-keeps-only-timed () + "Mixed list should keep only timed events, removing all-day ones." + (let* ((timed-time (test-time-tomorrow-at 14 30)) + (allday-time (test-time-days-from-now 2)) + (timed-ts (test-timestamp-string timed-time)) + (allday-ts (test-timestamp-string allday-time t)) + (times (list (cons timed-ts timed-time) + (cons allday-ts allday-time))) + (result (chime--filter-day-wide-events times))) + (should (= 1 (length result))) + (should (equal (car (car result)) timed-ts)))) + +(ert-deftest test-chime-filter-day-wide-events-multiple-timed-all-kept () + "Multiple timed events should all be kept." + (let* ((time1 (test-time-tomorrow-at 9 0)) + (time2 (test-time-tomorrow-at 14 30)) + (time3 (test-time-tomorrow-at 17 0)) + (times (list (cons (test-timestamp-string time1) time1) + (cons (test-timestamp-string time2) time2) + (cons (test-timestamp-string time3) time3))) + (result (chime--filter-day-wide-events times))) + (should (= 3 (length result))))) + +;;; Boundary Cases + +(ert-deftest test-chime-filter-day-wide-events-empty-list () + "Empty times list should return empty list." + (should (null (chime--filter-day-wide-events '())))) + +(ert-deftest test-chime-filter-day-wide-events-single-timed () + "Single timed event should return list of one." + (let* ((time (test-time-tomorrow-at 10 0)) + (ts (test-timestamp-string time)) + (result (chime--filter-day-wide-events (list (cons ts time))))) + (should (= 1 (length result))))) + +(ert-deftest test-chime-filter-day-wide-events-single-all-day () + "Single all-day event should return empty list." + (let* ((time (test-time-tomorrow-at 0 0)) + (ts (test-timestamp-string time t)) + (result (chime--filter-day-wide-events (list (cons ts time))))) + (should (null result)))) + +(ert-deftest test-chime-filter-day-wide-events-repeating-timed-kept () + "Repeating timestamp with time component should be kept." + (let* ((time (test-time-tomorrow-at 9 0)) + (ts (test-timestamp-repeating time "+1w")) + (result (chime--filter-day-wide-events (list (cons ts time))))) + (should (= 1 (length result))))) + +(ert-deftest test-chime-filter-day-wide-events-repeating-all-day-removed () + "Repeating timestamp without time component should be removed." + (let* ((time (test-time-tomorrow-at 0 0)) + (ts (test-timestamp-repeating time "+1y" t)) ;; all-day repeating + (result (chime--filter-day-wide-events (list (cons ts time))))) + (should (null result)))) + +;;; Error Cases + +(ert-deftest test-chime-filter-day-wide-events-all-entries-all-day () + "When all entries are all-day, should return empty list." + (let* ((time1 (test-time-tomorrow-at 0 0)) + (time2 (test-time-days-from-now 2)) + (times (list (cons (test-timestamp-string time1 t) time1) + (cons (test-timestamp-string time2 t) time2))) + (result (chime--filter-day-wide-events times))) + (should (null result)))) + +(provide 'test-chime-filter-day-wide-events) +;;; test-chime-filter-day-wide-events.el ends here diff --git a/tests/test-chime-get-tags.el b/tests/test-chime-get-tags.el new file mode 100644 index 0000000..75cad00 --- /dev/null +++ b/tests/test-chime-get-tags.el @@ -0,0 +1,158 @@ +;;; test-chime-get-tags.el --- Tests for chime--get-tags and chime-done-keywords-predicate -*- 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 tag extraction and done-keyword predicate: +;; - chime--get-tags: extracts org tags from a marker +;; - chime-done-keywords-predicate: checks if heading has a done keyword + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.el")) + +;;;; Tests for chime--get-tags + +;;; Normal Cases + +(ert-deftest test-chime-get-tags-single-tag () + "Should extract a single tag from an org heading." + (with-temp-buffer + (org-mode) + (insert "* Task :work:\n") + (goto-char (point-min)) + (let* ((marker (point-marker)) + (tags (chime--get-tags marker))) + (should (member "work" tags))))) + +(ert-deftest test-chime-get-tags-multiple-tags () + "Should extract multiple tags from an org heading." + (with-temp-buffer + (org-mode) + (insert "* Task :work:urgent:\n") + (goto-char (point-min)) + (let* ((marker (point-marker)) + (tags (chime--get-tags marker))) + (should (member "work" tags)) + (should (member "urgent" tags))))) + +;;; Boundary Cases + +(ert-deftest test-chime-get-tags-no-tags () + "Should return empty/nil list for heading with no tags." + (with-temp-buffer + (org-mode) + (insert "* Task without tags\n") + (goto-char (point-min)) + (let* ((marker (point-marker)) + (tags (chime--get-tags marker))) + ;; Should be empty (nil or empty list) + (should (null tags))))) + +(ert-deftest test-chime-get-tags-heading-with-todo-keyword () + "Should extract tags correctly even with TODO keyword present." + (with-temp-buffer + (org-mode) + (insert "* TODO Task :meeting:\n") + (goto-char (point-min)) + (let* ((marker (point-marker)) + (tags (chime--get-tags marker))) + (should (member "meeting" tags))))) + +;;;; Tests for chime-done-keywords-predicate + +;;; Normal Cases + +(ert-deftest test-chime-done-predicate-done-keyword () + "DONE heading should return truthy." + (with-temp-buffer + (org-mode) + (insert "* DONE Completed task\n") + (goto-char (point-min)) + (let ((marker (point-marker))) + (should (chime-done-keywords-predicate marker))))) + +(ert-deftest test-chime-done-predicate-todo-keyword () + "TODO heading should return nil." + (with-temp-buffer + (org-mode) + (insert "* TODO Active task\n") + (goto-char (point-min)) + (let ((marker (point-marker))) + (should-not (chime-done-keywords-predicate marker))))) + +(ert-deftest test-chime-done-predicate-no-keyword () + "Heading without any TODO keyword should return nil." + (with-temp-buffer + (org-mode) + (insert "* Plain heading\n") + (goto-char (point-min)) + (let ((marker (point-marker))) + (should-not (chime-done-keywords-predicate marker))))) + +;;; Boundary Cases + +(ert-deftest test-chime-done-predicate-custom-done-keywords () + "Custom done keywords (org-done-keywords) should be recognized." + (with-temp-buffer + (org-mode) + ;; CANCELLED is a standard done keyword in many org configs, + ;; but org-done-keywords defaults to just ("DONE"). + ;; Test with the default DONE keyword. + (insert "* DONE Task\n") + (goto-char (point-min)) + (let ((marker (point-marker))) + (should (member (nth 2 (org-heading-components)) org-done-keywords))))) + +(ert-deftest test-chime-done-predicate-heading-with-priority () + "DONE heading with priority should still be detected." + (with-temp-buffer + (org-mode) + (insert "* DONE [#A] High priority done task\n") + (goto-char (point-min)) + (let ((marker (point-marker))) + (should (chime-done-keywords-predicate marker))))) + +(ert-deftest test-chime-done-predicate-heading-with-tags () + "DONE heading with tags should still be detected." + (with-temp-buffer + (org-mode) + (insert "* DONE Tagged task :work:\n") + (goto-char (point-min)) + (let ((marker (point-marker))) + (should (chime-done-keywords-predicate marker))))) + +(provide 'test-chime-get-tags) +;;; test-chime-get-tags.el ends here diff --git a/tests/test-chime-log-silently.el b/tests/test-chime-log-silently.el new file mode 100644 index 0000000..e08987f --- /dev/null +++ b/tests/test-chime-log-silently.el @@ -0,0 +1,88 @@ +;;; test-chime-log-silently.el --- Tests for chime--log-silently -*- 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--log-silently function. +;; This function writes to *Messages* buffer without echoing to minibuffer. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;;; Normal Cases + +(ert-deftest test-chime-log-silently-writes-to-messages () + "Should write the formatted text to *Messages* buffer." + (let ((messages-buf (get-buffer-create "*Messages*"))) + (with-current-buffer messages-buf + (let ((pos-before (point-max))) + (chime--log-silently "Test log message") + (goto-char pos-before) + (should (search-forward "Test log message" nil t)))))) + +(ert-deftest test-chime-log-silently-formats-with-args () + "Should format with args like `format'." + (let ((messages-buf (get-buffer-create "*Messages*"))) + (with-current-buffer messages-buf + (let ((pos-before (point-max))) + (chime--log-silently "Event count: %d, name: %s" 5 "Meeting") + (goto-char pos-before) + (should (search-forward "Event count: 5, name: Meeting" nil t)))))) + +(ert-deftest test-chime-log-silently-multiple-calls-append () + "Multiple calls should append sequentially." + (let ((messages-buf (get-buffer-create "*Messages*"))) + (with-current-buffer messages-buf + (let ((pos-before (point-max))) + (chime--log-silently "First message") + (chime--log-silently "Second message") + (goto-char pos-before) + (let ((first-pos (search-forward "First message" nil t)) + (second-pos (progn (goto-char pos-before) + (search-forward "Second message" nil t)))) + (should first-pos) + (should second-pos) + ;; Second should come after first + (should (> second-pos first-pos))))))) + +;;; Boundary Cases + +(ert-deftest test-chime-log-silently-empty-format-string () + "Empty format string should not error." + ;; Should not signal an error + (should-not (condition-case nil + (progn (chime--log-silently "") nil) + (error t)))) + +(provide 'test-chime-log-silently) +;;; test-chime-log-silently.el ends here diff --git a/tests/test-chime-make-tooltip.el b/tests/test-chime-make-tooltip.el new file mode 100644 index 0000000..f51a3b3 --- /dev/null +++ b/tests/test-chime-make-tooltip.el @@ -0,0 +1,245 @@ +;;; test-chime-make-tooltip.el --- Tests for tooltip generation functions -*- 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 tooltip generation functions: +;; - chime--make-tooltip +;; - chime--make-no-events-tooltip + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; 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 + +(defun test-chime-make-tooltip-setup () + "Setup function run before each test." + (chime-create-test-base-dir) + (setq chime-modeline-tooltip-max-events 5) + (setq chime-tooltip-header-format "Upcoming Events as of %a %b %d %Y @ %I:%M %p") + (setq chime-display-time-format-string "%I:%M %p") + (setq chime-time-left-format-at-event "right now") + (setq chime-time-left-format-short "in %M") + (setq chime-time-left-format-long "in %H %M")) + +(defun test-chime-make-tooltip-teardown () + "Teardown function run after each test." + (chime-delete-test-base-dir)) + +;;; Helper to build upcoming-events list items +;;; Each item is (EVENT TIME-INFO MINUTES-UNTIL) where +;;; TIME-INFO is (TIMESTAMP-STR . TIME-OBJECT) + +(defun test-make-upcoming-item (title time minutes-until) + "Create an upcoming-events list item for TITLE at TIME, MINUTES-UNTIL from now." + (let ((ts (test-timestamp-string time))) + (list `((title . ,title) + (times . ((,ts . ,time))) + (intervals . ((10 . medium)))) + (cons ts time) + minutes-until))) + +;;;; Tests for chime--make-tooltip + +;;; Normal Cases + +(ert-deftest test-chime-make-tooltip-single-event () + "Single event should produce tooltip with header, day label, and event line." + (test-chime-make-tooltip-setup) + (unwind-protect + (let* ((now (test-time-now)) + (event-time (time-add now (seconds-to-time 1800))) ;; 30 min + (upcoming (list (test-make-upcoming-item "Team Meeting" event-time 30)))) + (with-test-time now + (let ((result (chime--make-tooltip upcoming))) + (should (stringp result)) + ;; Should contain header + (should (string-match-p "Upcoming Events" result)) + ;; Should contain the event title + (should (string-match-p "Team Meeting" result)) + ;; Should contain day separator + (should (string-match-p "─────────────" result))))) + (test-chime-make-tooltip-teardown))) + +(ert-deftest test-chime-make-tooltip-respects-max-events () + "Should respect chime-modeline-tooltip-max-events limit." + (test-chime-make-tooltip-setup) + (unwind-protect + (let* ((now (test-time-now)) + (upcoming (list + (test-make-upcoming-item "Event 1" (time-add now (seconds-to-time 600)) 10) + (test-make-upcoming-item "Event 2" (time-add now (seconds-to-time 1200)) 20) + (test-make-upcoming-item "Event 3" (time-add now (seconds-to-time 1800)) 30))) + (chime-modeline-tooltip-max-events 2)) + (with-test-time now + (let ((result (chime--make-tooltip upcoming))) + ;; Should show first 2 events + (should (string-match-p "Event 1" result)) + (should (string-match-p "Event 2" result)) + ;; Should NOT show 3rd event + (should-not (string-match-p "Event 3" result)) + ;; Should show "and 1 more" + (should (string-match-p "1 more event" result))))) + (test-chime-make-tooltip-teardown))) + +(ert-deftest test-chime-make-tooltip-and-n-more-pluralized () + "The 'and N more' text should use correct pluralization." + (test-chime-make-tooltip-setup) + (unwind-protect + (let* ((now (test-time-now)) + (upcoming (list + (test-make-upcoming-item "Event 1" (time-add now (seconds-to-time 600)) 10) + (test-make-upcoming-item "Event 2" (time-add now (seconds-to-time 1200)) 20) + (test-make-upcoming-item "Event 3" (time-add now (seconds-to-time 1800)) 30) + (test-make-upcoming-item "Event 4" (time-add now (seconds-to-time 2400)) 40))) + (chime-modeline-tooltip-max-events 2)) + (with-test-time now + (let ((result (chime--make-tooltip upcoming))) + ;; 2 remaining - should be "events" (plural) + (should (string-match-p "2 more events" result))))) + (test-chime-make-tooltip-teardown))) + +(ert-deftest test-chime-make-tooltip-nil-max-shows-all () + "When chime-modeline-tooltip-max-events is nil, all events should be shown." + (test-chime-make-tooltip-setup) + (unwind-protect + (let* ((now (test-time-now)) + (upcoming (list + (test-make-upcoming-item "Event 1" (time-add now (seconds-to-time 600)) 10) + (test-make-upcoming-item "Event 2" (time-add now (seconds-to-time 1200)) 20) + (test-make-upcoming-item "Event 3" (time-add now (seconds-to-time 1800)) 30))) + (chime-modeline-tooltip-max-events nil)) + (with-test-time now + (let ((result (chime--make-tooltip upcoming))) + (should (string-match-p "Event 1" result)) + (should (string-match-p "Event 2" result)) + (should (string-match-p "Event 3" result)) + ;; No "more" text + (should-not (string-match-p "more event" result))))) + (test-chime-make-tooltip-teardown))) + +;;; Boundary Cases + +(ert-deftest test-chime-make-tooltip-empty-list-returns-nil () + "Empty event list should return nil." + (should (null (chime--make-tooltip '())))) + +(ert-deftest test-chime-make-tooltip-nil-returns-nil () + "Nil input should return nil." + (should (null (chime--make-tooltip nil)))) + +(ert-deftest test-chime-make-tooltip-max-events-equals-count () + "When max-events equals event count, no 'more' text should appear." + (test-chime-make-tooltip-setup) + (unwind-protect + (let* ((now (test-time-now)) + (upcoming (list + (test-make-upcoming-item "Event 1" (time-add now (seconds-to-time 600)) 10) + (test-make-upcoming-item "Event 2" (time-add now (seconds-to-time 1200)) 20))) + (chime-modeline-tooltip-max-events 2)) + (with-test-time now + (let ((result (chime--make-tooltip upcoming))) + (should (string-match-p "Event 1" result)) + (should (string-match-p "Event 2" result)) + (should-not (string-match-p "more event" result))))) + (test-chime-make-tooltip-teardown))) + +;;;; Tests for chime--make-no-events-tooltip + +;;; Normal Cases + +(ert-deftest test-chime-no-events-tooltip-shows-minutes () + "For less than 60 minutes, should show timeframe in minutes." + (let ((result (chime--make-no-events-tooltip 30))) + (should (stringp result)) + (should (string-match-p "30 minutes" result)) + (should (string-match-p "Left-click: Open calendar" result)))) + +(ert-deftest test-chime-no-events-tooltip-shows-hours () + "For 2+ hours, should show timeframe in hours." + (let ((result (chime--make-no-events-tooltip 120))) + (should (stringp result)) + (should (string-match-p "2 hours" result)))) + +(ert-deftest test-chime-no-events-tooltip-shows-days () + "For 7+ days, should show timeframe in days." + (let ((result (chime--make-no-events-tooltip 10080))) ;; 7 days + (should (stringp result)) + (should (string-match-p "7 days" result)))) + +(ert-deftest test-chime-no-events-tooltip-shows-fractional-days () + "For 1-7 days (24-167 hours), should show fractional days." + (let ((result (chime--make-no-events-tooltip 2160))) ;; 36 hours = 1.5 days + (should (stringp result)) + (should (string-match-p "1\\.5 days" result)))) + +(ert-deftest test-chime-no-events-tooltip-includes-header () + "Tooltip should include the header from chime-tooltip-header-format." + (let ((result (chime--make-no-events-tooltip 60))) + (should (string-match-p "Upcoming Events" result)))) + +(ert-deftest test-chime-no-events-tooltip-mentions-config-var () + "Tooltip should mention chime-tooltip-lookahead-hours for user guidance." + (let ((result (chime--make-no-events-tooltip 60))) + (should (string-match-p "chime-tooltip-lookahead-hours" result)))) + +;;; Boundary Cases + +(ert-deftest test-chime-no-events-tooltip-exactly-60-minutes () + "Exactly 60 minutes (1 hour) should say '1 hour' not '1 hours'." + (let ((result (chime--make-no-events-tooltip 60))) + (should (stringp result)) + ;; A user expects correct English: "1 hour" not "1 hours" + (should (string-match-p "1 hour[^s]" result)))) + +(ert-deftest test-chime-no-events-tooltip-exactly-1-day () + "Exactly 1440 minutes (24 hours / 1 day) should not say '1.0 days'." + (let ((result (chime--make-no-events-tooltip 1440))) + (should (stringp result)) + ;; User expects "1 day" or "1.0 day" — not "1.0 days" + (should (string-match-p "1\\.0 day[^s]" result)))) + +(ert-deftest test-chime-no-events-tooltip-exactly-1-minute () + "1 minute should say '1 minute' not '1 minutes'." + (let ((result (chime--make-no-events-tooltip 1))) + (should (stringp result)) + ;; User expects "1 minute" not "1 minutes" + (should (string-match-p "1 minute[^s]" result)))) + +(provide 'test-chime-make-tooltip) +;;; test-chime-make-tooltip.el ends here diff --git a/tests/test-chime-propertize-modeline.el b/tests/test-chime-propertize-modeline.el new file mode 100644 index 0000000..fc210b2 --- /dev/null +++ b/tests/test-chime-propertize-modeline.el @@ -0,0 +1,163 @@ +;;; test-chime-propertize-modeline.el --- Tests for chime--propertize-modeline-string -*- 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--propertize-modeline-string function. +;; This function adds tooltip, click handlers, and mouse-face to modeline text. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; 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/Teardown + +(defun test-chime-propertize-setup () + "Setup function." + (chime-create-test-base-dir) + (setq chime-tooltip-header-format "Upcoming Events as of %a %b %d %Y @ %I:%M %p") + (setq chime-display-time-format-string "%I:%M %p") + (setq chime-time-left-format-at-event "right now") + (setq chime-time-left-format-short "in %M") + (setq chime-time-left-format-long "in %H %M")) + +(defun test-chime-propertize-teardown () + "Teardown function." + (chime-delete-test-base-dir) + (setq chime--upcoming-events nil)) + +;;; Normal Cases + +(ert-deftest test-chime-propertize-adds-help-echo () + "Should add help-echo property (tooltip) when events exist." + (test-chime-propertize-setup) + (unwind-protect + (let* ((now (test-time-now)) + (event-time (time-add now (seconds-to-time 1800))) + (ts (test-timestamp-string event-time))) + (setq chime--upcoming-events + (list (list `((title . "Meeting") (times . ((,ts . ,event-time)))) + (cons ts event-time) + 30))) + (with-test-time now + (let ((result (chime--propertize-modeline-string " ⏰ Meeting"))) + (should (get-text-property 0 'help-echo result))))) + (test-chime-propertize-teardown))) + +(ert-deftest test-chime-propertize-adds-mouse-face () + "Should add mouse-face property for highlight on hover." + (test-chime-propertize-setup) + (unwind-protect + (let* ((now (test-time-now)) + (event-time (time-add now (seconds-to-time 1800))) + (ts (test-timestamp-string event-time))) + (setq chime--upcoming-events + (list (list `((title . "Meeting") (times . ((,ts . ,event-time)))) + (cons ts event-time) + 30))) + (with-test-time now + (let ((result (chime--propertize-modeline-string " ⏰ Meeting"))) + (should (eq 'mode-line-highlight + (get-text-property 0 'mouse-face result)))))) + (test-chime-propertize-teardown))) + +(ert-deftest test-chime-propertize-adds-keymap () + "Should add local-map with click handlers." + (test-chime-propertize-setup) + (unwind-protect + (let* ((now (test-time-now)) + (event-time (time-add now (seconds-to-time 1800))) + (ts (test-timestamp-string event-time))) + (setq chime--upcoming-events + (list (list `((title . "Meeting") (times . ((,ts . ,event-time)))) + (cons ts event-time) + 30))) + (with-test-time now + (let ((result (chime--propertize-modeline-string " ⏰ Meeting"))) + (should (keymapp (get-text-property 0 'local-map result)))))) + (test-chime-propertize-teardown))) + +(ert-deftest test-chime-propertize-tooltip-contains-event () + "Tooltip text should contain the event title." + (test-chime-propertize-setup) + (unwind-protect + (let* ((now (test-time-now)) + (event-time (time-add now (seconds-to-time 1800))) + (ts (test-timestamp-string event-time))) + (setq chime--upcoming-events + (list (list `((title . "Team Standup") (times . ((,ts . ,event-time)))) + (cons ts event-time) + 30))) + (with-test-time now + (let* ((result (chime--propertize-modeline-string " ⏰ Meeting")) + (tooltip (get-text-property 0 'help-echo result))) + (should (string-match-p "Team Standup" tooltip))))) + (test-chime-propertize-teardown))) + +;;; Boundary Cases + +(ert-deftest test-chime-propertize-nil-events-returns-plain-text () + "When chime--upcoming-events is nil, should return plain text without properties." + (test-chime-propertize-setup) + (unwind-protect + (let ((chime--upcoming-events nil)) + (let ((result (chime--propertize-modeline-string " ⏰"))) + ;; Should return the text as-is + (should (string= " ⏰" result)) + ;; Should NOT have help-echo (no tooltip) + (should-not (get-text-property 0 'help-echo result)))) + (test-chime-propertize-teardown))) + +(ert-deftest test-chime-propertize-empty-text () + "Empty string should still get properties when events exist." + (test-chime-propertize-setup) + (unwind-protect + (let* ((now (test-time-now)) + (event-time (time-add now (seconds-to-time 1800))) + (ts (test-timestamp-string event-time))) + (setq chime--upcoming-events + (list (list `((title . "Meeting") (times . ((,ts . ,event-time)))) + (cons ts event-time) + 30))) + (with-test-time now + (let ((result (chime--propertize-modeline-string ""))) + ;; Even empty string gets propertized + (should (stringp result))))) + (test-chime-propertize-teardown))) + +(provide 'test-chime-propertize-modeline) +;;; test-chime-propertize-modeline.el ends here diff --git a/tests/test-chime-time-utilities.el b/tests/test-chime-time-utilities.el new file mode 100644 index 0000000..06e1a3b --- /dev/null +++ b/tests/test-chime-time-utilities.el @@ -0,0 +1,174 @@ +;;; test-chime-time-utilities.el --- Tests for time utility functions -*- 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 time utility functions: +;; - chime-get-minutes-into-day +;; - chime-get-hours-minutes-from-time +;; - chime-set-hours-minutes-for-time + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;; Load test utilities +(require 'testutil-general (expand-file-name "testutil-general.el")) +(require 'testutil-time (expand-file-name "testutil-time.el")) + +;;;; Tests for chime-get-minutes-into-day + +;;; Normal Cases + +(ert-deftest test-chime-get-minutes-into-day-noon () + "Noon (12:00) should be 720 minutes into the day." + (should (= 720 (chime-get-minutes-into-day "12:00")))) + +(ert-deftest test-chime-get-minutes-into-day-afternoon () + "14:30 should be 870 minutes into the day." + (should (= 870 (chime-get-minutes-into-day "14:30")))) + +(ert-deftest test-chime-get-minutes-into-day-morning () + "8:00 should be 480 minutes into the day." + (should (= 480 (chime-get-minutes-into-day "8:00")))) + +;;; Boundary Cases + +(ert-deftest test-chime-get-minutes-into-day-midnight () + "Midnight (0:00) should be 0 minutes into the day." + (should (= 0 (chime-get-minutes-into-day "0:00")))) + +(ert-deftest test-chime-get-minutes-into-day-end-of-day () + "23:59 should be 1439 minutes into the day." + (should (= 1439 (chime-get-minutes-into-day "23:59")))) + +(ert-deftest test-chime-get-minutes-into-day-one-minute-past-midnight () + "0:01 should be 1 minute into the day." + (should (= 1 (chime-get-minutes-into-day "0:01")))) + +;;;; Tests for chime-get-hours-minutes-from-time + +;;; Normal Cases + +(ert-deftest test-chime-get-hours-minutes-afternoon () + "14:30 should return (14 30)." + (should (equal '(14 30) (chime-get-hours-minutes-from-time "14:30")))) + +(ert-deftest test-chime-get-hours-minutes-morning () + "8:00 should return (8 0)." + (should (equal '(8 0) (chime-get-hours-minutes-from-time "8:00")))) + +(ert-deftest test-chime-get-hours-minutes-with-minutes () + "9:45 should return (9 45)." + (should (equal '(9 45) (chime-get-hours-minutes-from-time "9:45")))) + +;;; Boundary Cases + +(ert-deftest test-chime-get-hours-minutes-midnight () + "0:00 should return (0 0)." + (should (equal '(0 0) (chime-get-hours-minutes-from-time "0:00")))) + +(ert-deftest test-chime-get-hours-minutes-exact-hour () + "10:00 should return (10 0) with no leftover minutes." + (should (equal '(10 0) (chime-get-hours-minutes-from-time "10:00")))) + +(ert-deftest test-chime-get-hours-minutes-end-of-day () + "23:59 should return (23 59)." + (should (equal '(23 59) (chime-get-hours-minutes-from-time "23:59")))) + +(ert-deftest test-chime-get-hours-minutes-noon () + "12:00 should return (12 0)." + (should (equal '(12 0) (chime-get-hours-minutes-from-time "12:00")))) + +;;;; Tests for chime-set-hours-minutes-for-time + +;;; Normal Cases + +(ert-deftest test-chime-set-hours-minutes-preserves-date () + "Setting hours/minutes should preserve the date." + (let* ((base (test-time-tomorrow-at 10 0)) + (result (chime-set-hours-minutes-for-time base 14 30)) + (decoded (decode-time result)) + (base-decoded (decode-time base))) + ;; Date should be the same + (should (= (decoded-time-day base-decoded) (decoded-time-day decoded))) + (should (= (decoded-time-month base-decoded) (decoded-time-month decoded))) + (should (= (decoded-time-year base-decoded) (decoded-time-year decoded))) + ;; Time should be changed + (should (= 14 (decoded-time-hour decoded))) + (should (= 30 (decoded-time-minute decoded))) + (should (= 0 (decoded-time-second decoded))))) + +(ert-deftest test-chime-set-hours-minutes-changes-time () + "Setting different hours/minutes should produce different time." + (let* ((base (test-time-tomorrow-at 10 0)) + (result (chime-set-hours-minutes-for-time base 15 45)) + (decoded (decode-time result))) + (should (= 15 (decoded-time-hour decoded))) + (should (= 45 (decoded-time-minute decoded))))) + +;;; Boundary Cases + +(ert-deftest test-chime-set-hours-minutes-midnight () + "Setting to midnight (0, 0) should work." + (let* ((base (test-time-tomorrow-at 10 0)) + (result (chime-set-hours-minutes-for-time base 0 0)) + (decoded (decode-time result))) + (should (= 0 (decoded-time-hour decoded))) + (should (= 0 (decoded-time-minute decoded))))) + +(ert-deftest test-chime-set-hours-minutes-end-of-day () + "Setting to 23:59 should work." + (let* ((base (test-time-tomorrow-at 10 0)) + (result (chime-set-hours-minutes-for-time base 23 59)) + (decoded (decode-time result))) + (should (= 23 (decoded-time-hour decoded))) + (should (= 59 (decoded-time-minute decoded))))) + +(ert-deftest test-chime-set-hours-minutes-roundtrip () + "Extracting hours/minutes and setting them back should produce same time-of-day." + (let* ((base (test-time-tomorrow-at 14 30)) + (hm (chime-get-hours-minutes-from-time "14:30")) + (result (chime-set-hours-minutes-for-time base (car hm) (cadr hm))) + (decoded (decode-time result))) + (should (= 14 (decoded-time-hour decoded))) + (should (= 30 (decoded-time-minute decoded))))) + +(ert-deftest test-chime-set-hours-minutes-seconds-always-zero () + "Seconds should always be set to 0 regardless of base time." + (let* ((base (test-time-now)) ;; may have non-zero seconds internally + (result (chime-set-hours-minutes-for-time base 10 0)) + (decoded (decode-time result))) + (should (= 0 (decoded-time-second decoded))))) + +(provide 'test-chime-time-utilities) +;;; test-chime-time-utilities.el ends here diff --git a/tests/test-chime-warn-persistent-failures.el b/tests/test-chime-warn-persistent-failures.el new file mode 100644 index 0000000..e24132a --- /dev/null +++ b/tests/test-chime-warn-persistent-failures.el @@ -0,0 +1,165 @@ +;;; test-chime-warn-persistent-failures.el --- Tests for chime--maybe-warn-persistent-failures -*- 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--maybe-warn-persistent-failures function. +;; This function warns the user when consecutive async failures reach +;; the configured threshold. It should warn exactly once at the threshold. + +;;; Code: + +;; Initialize package system for batch mode +(when noninteractive + (package-initialize)) + +(require 'ert) + +;; Load dependencies required by chime +(require 'dash) +(require 'alert) +(require 'async) +(require 'org-agenda) + +;; Load chime from parent directory +(load (expand-file-name "../chime.el") nil t) + +;;; Setup and Teardown + +(defun test-warn-failures-setup () + "Setup function." + (setq chime--consecutive-async-failures 0) + (setq chime-max-consecutive-failures 5)) + +(defun test-warn-failures-teardown () + "Teardown function." + (setq chime--consecutive-async-failures 0) + (setq chime-max-consecutive-failures 5)) + +;;; Normal Cases + +(ert-deftest test-chime-warn-failures-warns-at-threshold () + "Should call display-warning when failures reach the threshold." + (test-warn-failures-setup) + (unwind-protect + (let ((warned nil)) + (setq chime--consecutive-async-failures 5) + (setq chime-max-consecutive-failures 5) + (cl-letf (((symbol-function 'display-warning) + (lambda (_type _msg &rest _args) (setq warned t)))) + (chime--maybe-warn-persistent-failures) + (should warned))) + (test-warn-failures-teardown))) + +(ert-deftest test-chime-warn-failures-no-warn-below-threshold () + "Should NOT warn when failures are below the threshold." + (test-warn-failures-setup) + (unwind-protect + (let ((warned nil)) + (setq chime--consecutive-async-failures 4) + (setq chime-max-consecutive-failures 5) + (cl-letf (((symbol-function 'display-warning) + (lambda (_type _msg &rest _args) (setq warned t)))) + (chime--maybe-warn-persistent-failures) + (should-not warned))) + (test-warn-failures-teardown))) + +(ert-deftest test-chime-warn-failures-no-warn-above-threshold () + "Should NOT warn again after exceeding the threshold (warns once)." + (test-warn-failures-setup) + (unwind-protect + (let ((warned nil)) + (setq chime--consecutive-async-failures 6) + (setq chime-max-consecutive-failures 5) + (cl-letf (((symbol-function 'display-warning) + (lambda (_type _msg &rest _args) (setq warned t)))) + (chime--maybe-warn-persistent-failures) + (should-not warned))) + (test-warn-failures-teardown))) + +(ert-deftest test-chime-warn-failures-message-includes-count () + "Warning message should include the failure count." + (test-warn-failures-setup) + (unwind-protect + (let ((warning-msg nil)) + (setq chime--consecutive-async-failures 5) + (setq chime-max-consecutive-failures 5) + (cl-letf (((symbol-function 'display-warning) + (lambda (_type msg &rest _args) (setq warning-msg msg)))) + (chime--maybe-warn-persistent-failures) + (should warning-msg) + (should (string-match-p "5" warning-msg)))) + (test-warn-failures-teardown))) + +(ert-deftest test-chime-warn-failures-severity-is-warning () + "Warning should use :warning severity." + (test-warn-failures-setup) + (unwind-protect + (let ((warning-severity nil)) + (setq chime--consecutive-async-failures 5) + (setq chime-max-consecutive-failures 5) + (cl-letf (((symbol-function 'display-warning) + (lambda (_type _msg &rest args) (setq warning-severity (car args))))) + (chime--maybe-warn-persistent-failures) + (should (eq :warning warning-severity)))) + (test-warn-failures-teardown))) + +;;; Boundary Cases + +(ert-deftest test-chime-warn-failures-threshold-of-1 () + "Threshold of 1 should warn on the very first failure." + (test-warn-failures-setup) + (unwind-protect + (let ((warned nil)) + (setq chime--consecutive-async-failures 1) + (setq chime-max-consecutive-failures 1) + (cl-letf (((symbol-function 'display-warning) + (lambda (_type _msg &rest _args) (setq warned t)))) + (chime--maybe-warn-persistent-failures) + (should warned))) + (test-warn-failures-teardown))) + +(ert-deftest test-chime-warn-failures-threshold-of-0-disables () + "Threshold of 0 should disable warnings entirely." + (test-warn-failures-setup) + (unwind-protect + (let ((warned nil)) + (setq chime--consecutive-async-failures 0) + (setq chime-max-consecutive-failures 0) + (cl-letf (((symbol-function 'display-warning) + (lambda (_type _msg &rest _args) (setq warned t)))) + (chime--maybe-warn-persistent-failures) + (should-not warned))) + (test-warn-failures-teardown))) + +(ert-deftest test-chime-warn-failures-zero-failures-no-warn () + "Zero failures should never warn regardless of threshold." + (test-warn-failures-setup) + (unwind-protect + (let ((warned nil)) + (setq chime--consecutive-async-failures 0) + (setq chime-max-consecutive-failures 5) + (cl-letf (((symbol-function 'display-warning) + (lambda (_type _msg &rest _args) (setq warned t)))) + (chime--maybe-warn-persistent-failures) + (should-not warned))) + (test-warn-failures-teardown))) + +(provide 'test-chime-warn-persistent-failures) +;;; test-chime-warn-persistent-failures.el ends here |
