summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tests/test-wttrin--mode-line-tooltip.el174
-rw-r--r--tests/test-wttrin--mode-line-update-display.el6
-rw-r--r--wttrin.el40
3 files changed, 204 insertions, 16 deletions
diff --git a/tests/test-wttrin--mode-line-tooltip.el b/tests/test-wttrin--mode-line-tooltip.el
new file mode 100644
index 0000000..b92fb8f
--- /dev/null
+++ b/tests/test-wttrin--mode-line-tooltip.el
@@ -0,0 +1,174 @@
+;;; test-wttrin--mode-line-tooltip.el --- Tests for mode-line tooltip -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025-2026 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for wttrin--mode-line-tooltip and dynamic help-echo behavior.
+;; The tooltip should compute age at hover time, not at fetch time, so the
+;; user sees accurate "Updated X ago" text regardless of when they hover.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;;; Setup and Teardown
+
+(defun test-wttrin--mode-line-tooltip-setup ()
+ "Setup for mode-line tooltip tests."
+ (testutil-wttrin-setup)
+ (setq wttrin-mode-line-string nil)
+ (setq wttrin--mode-line-cache nil))
+
+(defun test-wttrin--mode-line-tooltip-teardown ()
+ "Teardown for mode-line tooltip tests."
+ (testutil-wttrin-teardown)
+ (setq wttrin-mode-line-string nil)
+ (setq wttrin--mode-line-cache nil))
+
+;;; --------------------------------------------------------------------------
+;;; wttrin--mode-line-tooltip
+;;; --------------------------------------------------------------------------
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin--mode-line-tooltip-normal-minutes-old ()
+ "Cache that is 5 minutes old should report '5 minutes ago'."
+ (test-wttrin--mode-line-tooltip-setup)
+ (unwind-protect
+ (cl-letf (((symbol-function 'float-time) (lambda () 1300.0)))
+ (setq wttrin--mode-line-cache (cons 1000.0 "Paris: ☀️ +61°F Clear"))
+ (let ((tooltip (wttrin--mode-line-tooltip)))
+ (should (string-match-p "5 minutes ago" tooltip))))
+ (test-wttrin--mode-line-tooltip-teardown)))
+
+(ert-deftest test-wttrin--mode-line-tooltip-normal-hours-old ()
+ "Cache that is 2 hours old should report '2 hours ago'."
+ (test-wttrin--mode-line-tooltip-setup)
+ (unwind-protect
+ (cl-letf (((symbol-function 'float-time) (lambda () 8200.0)))
+ (setq wttrin--mode-line-cache (cons 1000.0 "Paris: ☀️ +61°F Clear"))
+ (let ((tooltip (wttrin--mode-line-tooltip)))
+ (should (string-match-p "2 hours ago" tooltip))))
+ (test-wttrin--mode-line-tooltip-teardown)))
+
+(ert-deftest test-wttrin--mode-line-tooltip-normal-includes-weather-string ()
+ "Tooltip should include the full weather string so user sees conditions."
+ (test-wttrin--mode-line-tooltip-setup)
+ (unwind-protect
+ (cl-letf (((symbol-function 'float-time) (lambda () 1060.0)))
+ (setq wttrin--mode-line-cache (cons 1000.0 "Paris: ☀️ +61°F Clear"))
+ (let ((tooltip (wttrin--mode-line-tooltip)))
+ (should (string-match-p "Paris" tooltip))
+ (should (string-match-p "Clear" tooltip))))
+ (test-wttrin--mode-line-tooltip-teardown)))
+
+(ert-deftest test-wttrin--mode-line-tooltip-normal-stale-shows-stale-message ()
+ "Data older than 2x refresh interval should show stale warning."
+ (test-wttrin--mode-line-tooltip-setup)
+ (unwind-protect
+ (let ((wttrin-mode-line-refresh-interval 900))
+ (cl-letf (((symbol-function 'float-time) (lambda () 3000.0)))
+ ;; Age is 2000s, threshold is 2*900=1800 → stale
+ (setq wttrin--mode-line-cache (cons 1000.0 "Paris: ☀️ +61°F Clear"))
+ (let ((tooltip (wttrin--mode-line-tooltip)))
+ (should (string-match-p "Stale" tooltip))
+ (should (string-match-p "fetch failed" tooltip)))))
+ (test-wttrin--mode-line-tooltip-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin--mode-line-tooltip-boundary-just-fetched ()
+ "Cache less than 60 seconds old should say 'just now'."
+ (test-wttrin--mode-line-tooltip-setup)
+ (unwind-protect
+ (cl-letf (((symbol-function 'float-time) (lambda () 1030.0)))
+ (setq wttrin--mode-line-cache (cons 1000.0 "Paris: ☀️ +61°F Clear"))
+ (let ((tooltip (wttrin--mode-line-tooltip)))
+ (should (string-match-p "just now" tooltip))))
+ (test-wttrin--mode-line-tooltip-teardown)))
+
+(ert-deftest test-wttrin--mode-line-tooltip-boundary-nil-cache ()
+ "When cache is nil, tooltip function should return nil without crashing."
+ (test-wttrin--mode-line-tooltip-setup)
+ (unwind-protect
+ (progn
+ (setq wttrin--mode-line-cache nil)
+ (should-not (wttrin--mode-line-tooltip)))
+ (test-wttrin--mode-line-tooltip-teardown)))
+
+(ert-deftest test-wttrin--mode-line-tooltip-boundary-exactly-at-stale-threshold ()
+ "Age exactly at 2x refresh interval should NOT be stale (threshold is >)."
+ (test-wttrin--mode-line-tooltip-setup)
+ (unwind-protect
+ (let ((wttrin-mode-line-refresh-interval 900))
+ (cl-letf (((symbol-function 'float-time) (lambda () 2800.0)))
+ ;; Age is exactly 1800s = 2*900 → NOT stale (> not >=)
+ (setq wttrin--mode-line-cache (cons 1000.0 "Paris: ☀️ +61°F Clear"))
+ (let ((tooltip (wttrin--mode-line-tooltip)))
+ (should (string-match-p "Updated" tooltip))
+ (should-not (string-match-p "Stale" tooltip)))))
+ (test-wttrin--mode-line-tooltip-teardown)))
+
+;;; --------------------------------------------------------------------------
+;;; Integration: dynamic help-echo
+;;; --------------------------------------------------------------------------
+
+(ert-deftest test-wttrin--mode-line-tooltip-integration-help-echo-is-function ()
+ "After update-display, help-echo should be a function, not a static string."
+ (test-wttrin--mode-line-tooltip-setup)
+ (unwind-protect
+ (cl-letf (((symbol-function 'float-time) (lambda () 1000.0)))
+ (setq wttrin--mode-line-cache (cons 1000.0 "Paris: ☀️ +61°F Clear"))
+ (wttrin--mode-line-update-display)
+ (let ((help-echo (get-text-property 0 'help-echo wttrin-mode-line-string)))
+ (should (functionp help-echo))))
+ (test-wttrin--mode-line-tooltip-teardown)))
+
+(ert-deftest test-wttrin--mode-line-tooltip-integration-age-updates-over-time ()
+ "Same cache, different times: tooltip should show different ages.
+This is the bug reproduction — previously the tooltip was frozen at 'just now'."
+ (test-wttrin--mode-line-tooltip-setup)
+ (unwind-protect
+ (progn
+ ;; Cache data at time 1000
+ (setq wttrin--mode-line-cache (cons 1000.0 "Paris: ☀️ +61°F Clear"))
+
+ ;; At time 1000: should say "just now"
+ (cl-letf (((symbol-function 'float-time) (lambda () 1000.0)))
+ (wttrin--mode-line-update-display)
+ (let* ((help-echo-fn (get-text-property 0 'help-echo wttrin-mode-line-string))
+ (tooltip (funcall help-echo-fn nil nil nil)))
+ (should (string-match-p "just now" tooltip))))
+
+ ;; At time 1720 (12 minutes later), WITHOUT re-fetching: should say "12 minutes ago"
+ (cl-letf (((symbol-function 'float-time) (lambda () 1720.0)))
+ (let* ((help-echo-fn (get-text-property 0 'help-echo wttrin-mode-line-string))
+ (tooltip (funcall help-echo-fn nil nil nil)))
+ (should (string-match-p "12 minutes ago" tooltip)))))
+ (test-wttrin--mode-line-tooltip-teardown)))
+
+;;; Error Cases
+
+(ert-deftest test-wttrin--mode-line-tooltip-error-cache-cleared-after-display ()
+ "If cache is cleared after display was set, hovering should not crash."
+ (test-wttrin--mode-line-tooltip-setup)
+ (unwind-protect
+ (progn
+ (cl-letf (((symbol-function 'float-time) (lambda () 1000.0)))
+ (setq wttrin--mode-line-cache (cons 1000.0 "Paris: ☀️ +61°F Clear"))
+ (wttrin--mode-line-update-display))
+ ;; Cache cleared (e.g., by wttrin--mode-line-stop)
+ (setq wttrin--mode-line-cache nil)
+ ;; Hovering over the now-stale mode-line string should not crash
+ (let ((help-echo-fn (get-text-property 0 'help-echo wttrin-mode-line-string)))
+ (should (functionp help-echo-fn))
+ ;; Should return nil or a string, not crash
+ (let ((result (funcall help-echo-fn nil nil nil)))
+ (should (or (null result) (stringp result))))))
+ (test-wttrin--mode-line-tooltip-teardown)))
+
+(provide 'test-wttrin--mode-line-tooltip)
+;;; test-wttrin--mode-line-tooltip.el ends here
diff --git a/tests/test-wttrin--mode-line-update-display.el b/tests/test-wttrin--mode-line-update-display.el
index e23780b..81fbded 100644
--- a/tests/test-wttrin--mode-line-update-display.el
+++ b/tests/test-wttrin--mode-line-update-display.el
@@ -146,7 +146,8 @@
(cl-letf (((symbol-function 'float-time) (lambda () 1300.0)))
(setq wttrin--mode-line-cache (cons 1000.0 "Paris: ☀️ +61°F Clear"))
(wttrin--mode-line-update-display)
- (let ((tooltip (get-text-property 0 'help-echo wttrin-mode-line-string)))
+ (let* ((help-echo-fn (get-text-property 0 'help-echo wttrin-mode-line-string))
+ (tooltip (funcall help-echo-fn nil nil nil)))
(should (string-match-p "Paris" tooltip))
(should (string-match-p "Updated 5 minutes ago" tooltip))))
(test-wttrin--mode-line-update-display-teardown)))
@@ -162,7 +163,8 @@
;; Data is 2000 seconds old, threshold is 2*900=1800 -> stale
(setq wttrin--mode-line-cache (cons 1000.0 "Paris: ☀️ +61°F Clear"))
(wttrin--mode-line-update-display)
- (let ((tooltip (get-text-property 0 'help-echo wttrin-mode-line-string)))
+ (let* ((help-echo-fn (get-text-property 0 'help-echo wttrin-mode-line-string))
+ (tooltip (funcall help-echo-fn nil nil nil)))
(should (string-match-p "Stale" tooltip))
(should (string-match-p "fetch failed" tooltip)))))
(test-wttrin--mode-line-update-display-teardown)))
diff --git a/wttrin.el b/wttrin.el
index 972a2e8..0af5e50 100644
--- a/wttrin.el
+++ b/wttrin.el
@@ -592,6 +592,21 @@ On failure with no cache, shows error placeholder."
;; No cache at all — show error placeholder
(wttrin--mode-line-update-placeholder-error))))))))
+(defun wttrin--mode-line-tooltip (&optional _window _object _pos)
+ "Compute tooltip text from `wttrin--mode-line-cache'.
+Calculates age at call time so the tooltip is always current.
+Optional arguments are ignored (required by `help-echo' function protocol)."
+ (when wttrin--mode-line-cache
+ (let* ((timestamp (car wttrin--mode-line-cache))
+ (weather-string (cdr wttrin--mode-line-cache))
+ (age (- (float-time) timestamp))
+ (stale-p (> age (* 2 wttrin-mode-line-refresh-interval)))
+ (age-str (wttrin--format-age age)))
+ (if stale-p
+ (format "%s\nStale: updated %s — fetch failed, will retry"
+ weather-string age-str)
+ (format "%s\nUpdated %s" weather-string age-str)))))
+
(defun wttrin--mode-line-update-display ()
"Update mode-line display from `wttrin--mode-line-cache'.
Reads cached weather data, computes age, and sets the mode-line string.
@@ -601,23 +616,20 @@ shows staleness info in tooltip."
(let* ((timestamp (car wttrin--mode-line-cache))
(weather-string (cdr wttrin--mode-line-cache))
(age (- (float-time) timestamp))
- (stale-p (> age (* 2 wttrin-mode-line-refresh-interval)))
- (age-str (wttrin--format-age age)))
- (wttrin--debug-log "mode-line-display: Updating from cache, age=%s, stale=%s"
- age-str stale-p)
+ (stale-p (> age (* 2 wttrin-mode-line-refresh-interval))))
+ (wttrin--debug-log "mode-line-display: Updating from cache, stale=%s" stale-p)
;; Extract just the emoji for mode-line display
- (let* ((emoji (if (string-match ":\\s-*\\(.\\)" weather-string)
- (match-string 1 weather-string)
- "?"))
- (tooltip (if stale-p
- (format "%s\nStale: updated %s — fetch failed, will retry"
- weather-string age-str)
- (format "%s\nUpdated %s" weather-string age-str))))
+ (let ((emoji (if (string-match ":\\s-*\\(.\\)" weather-string)
+ (match-string 1 weather-string)
+ "?")))
(wttrin--debug-log "mode-line-display: Extracted emoji = %S, stale = %s"
emoji stale-p)
- (wttrin--set-mode-line-string
- (wttrin--make-emoji-icon emoji (when stale-p "gray60"))
- tooltip)))))
+ (setq wttrin-mode-line-string
+ (propertize (concat " " (wttrin--make-emoji-icon emoji (when stale-p "gray60")))
+ 'help-echo #'wttrin--mode-line-tooltip
+ 'mouse-face 'mode-line-highlight
+ 'local-map wttrin--mode-line-map)))))
+ (force-mode-line-update t))
(defun wttrin-mode-line-click ()
"Handle left-click on mode-line weather widget.