aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-02-21 07:06:50 -0600
committerCraig Jennings <c@cjennings.net>2026-02-21 07:11:03 -0600
commitb74b98f177d92d50ddbede900ba41212e07c5f63 (patch)
tree459b1630dcc7d1c941f850565acdc16332831948 /tests
parentec8130cfe1a7390e9939b311c8db39907a3f7f44 (diff)
downloademacs-wttrin-b74b98f177d92d50ddbede900ba41212e07c5f63.tar.gz
emacs-wttrin-b74b98f177d92d50ddbede900ba41212e07c5f63.zip
feat: unified cache and staleness handling for mode-line and buffer
Replace TTL-based cache invalidation with proactive scheduled refresh. Both mode-line and buffer systems now follow: timer refreshes cache, display reads from cache, staleness indicated when data is old. Phase 1 - Mode-line cache formalization + staleness display: - Replace wttrin--mode-line-tooltip-data with wttrin--mode-line-cache as (timestamp . data) cons cell matching buffer cache pattern - Add wttrin--format-age helper for human-readable age strings - Rewrite wttrin--mode-line-update-display to take no arguments, read from cache, compute staleness (age > 2x refresh interval), dim emoji gray when stale, show staleness info in tooltip - Rewrite wttrin--mode-line-fetch-weather to write cache on success, show stale display on failure with cache, error placeholder without - Add wttrin--mode-line-update-placeholder-error for first-launch failure Phase 2 - Remove TTL, add proactive buffer refresh: - Rename wttrin-cache-ttl to wttrin-refresh-interval (default 3600s) with define-obsolete-variable-alias for backward compatibility - Change wttrin-mode-line-refresh-interval default from 900 to 3600 - Remove TTL check from wttrin--get-cached-or-fetch; serve cached data regardless of age, background timer keeps it fresh - Add buffer refresh timer (wttrin--buffer-cache-refresh) Phase 3 - Buffer staleness display: - Add wttrin--format-staleness-header for buffer age display - Insert staleness line in wttrin--display-weather before instructions Phase 4 - Cleanup: - Remove all references to wttrin--mode-line-tooltip-data - Update README.org cache settings and mode-line documentation - Update tests for new API (198 tests across 21 files, all passing)
Diffstat (limited to 'tests')
-rw-r--r--tests/test-integration-debug.el8
-rw-r--r--tests/test-wttrin--format-staleness-header.el80
-rw-r--r--tests/test-wttrin--get-cached-or-fetch.el109
-rw-r--r--tests/test-wttrin--mode-line-map.el11
-rw-r--r--tests/test-wttrin--mode-line-update-display.el275
-rw-r--r--tests/test-wttrin-smoke.el2
-rw-r--r--tests/testutil-wttrin.el15
7 files changed, 320 insertions, 180 deletions
diff --git a/tests/test-integration-debug.el b/tests/test-integration-debug.el
index db2706b..d4abd9b 100644
--- a/tests/test-integration-debug.el
+++ b/tests/test-integration-debug.el
@@ -45,7 +45,7 @@
(wttrin-clear-cache)
(wttrin-debug-clear-log)
(setq wttrin-mode-line-string nil)
- (setq wttrin--mode-line-tooltip-data nil))
+ (setq wttrin--mode-line-cache nil))
;;; Mock URL Fetching
@@ -104,9 +104,9 @@ and emoji-extraction events to the debug log."
(should (stringp wttrin-mode-line-string))
(should (string-match-p "☀" wttrin-mode-line-string)) ; Should contain emoji
- ;; Verify tooltip data was set
- (should wttrin--mode-line-tooltip-data)
- (should (string= test-wttrin-sample-weather-data wttrin--mode-line-tooltip-data))
+ ;; Verify cache was populated with weather data
+ (should wttrin--mode-line-cache)
+ (should (string= test-wttrin-sample-weather-data (cdr wttrin--mode-line-cache)))
;; Verify debug log captured key events
(let ((log-messages (mapcar #'cdr wttrin--debug-log)))
diff --git a/tests/test-wttrin--format-staleness-header.el b/tests/test-wttrin--format-staleness-header.el
new file mode 100644
index 0000000..ef8882f
--- /dev/null
+++ b/tests/test-wttrin--format-staleness-header.el
@@ -0,0 +1,80 @@
+;;; test-wttrin--format-staleness-header.el --- Tests for staleness header -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for wttrin--format-staleness-header function.
+;; Tests that buffer staleness information is formatted correctly.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;;; Setup and Teardown
+
+(defun test-wttrin--format-staleness-header-setup ()
+ "Setup for staleness header tests."
+ (testutil-wttrin-setup))
+
+(defun test-wttrin--format-staleness-header-teardown ()
+ "Teardown for staleness header tests."
+ (testutil-wttrin-teardown))
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin--format-staleness-header-normal-returns-formatted-string ()
+ "Staleness header includes time and age when cache exists."
+ (test-wttrin--format-staleness-header-setup)
+ (unwind-protect
+ (let ((now 1000000.0))
+ (cl-letf (((symbol-function 'float-time) (lambda () now)))
+ ;; Add data cached 5 minutes ago
+ (testutil-wttrin-add-to-cache "Paris" "weather data" 300)
+ (let ((header (wttrin--format-staleness-header "Paris")))
+ (should header)
+ (should (string-match-p "Last updated:" header))
+ (should (string-match-p "5 minutes ago" header)))))
+ (test-wttrin--format-staleness-header-teardown)))
+
+(ert-deftest test-wttrin--format-staleness-header-normal-just-now ()
+ "Staleness header shows 'just now' for very recent data."
+ (test-wttrin--format-staleness-header-setup)
+ (unwind-protect
+ (let ((now 1000000.0))
+ (cl-letf (((symbol-function 'float-time) (lambda () now)))
+ (testutil-wttrin-add-to-cache "Paris" "weather data" 10)
+ (let ((header (wttrin--format-staleness-header "Paris")))
+ (should (string-match-p "just now" header)))))
+ (test-wttrin--format-staleness-header-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin--format-staleness-header-boundary-no-cache-returns-nil ()
+ "Returns nil when no cache entry exists for location."
+ (test-wttrin--format-staleness-header-setup)
+ (unwind-protect
+ (should-not (wttrin--format-staleness-header "Unknown City"))
+ (test-wttrin--format-staleness-header-teardown)))
+
+(ert-deftest test-wttrin--format-staleness-header-boundary-different-unit-system ()
+ "Cache lookup respects current unit system setting."
+ (test-wttrin--format-staleness-header-setup)
+ (unwind-protect
+ (let ((now 1000000.0))
+ (cl-letf (((symbol-function 'float-time) (lambda () now)))
+ ;; Add data under metric unit system
+ (let ((wttrin-unit-system "m"))
+ (testutil-wttrin-add-to-cache "Paris" "metric data" 300))
+ ;; Query under imperial unit system — should find nothing
+ (let ((wttrin-unit-system "u"))
+ (should-not (wttrin--format-staleness-header "Paris")))
+ ;; Query under metric — should find it
+ (let ((wttrin-unit-system "m"))
+ (should (wttrin--format-staleness-header "Paris")))))
+ (test-wttrin--format-staleness-header-teardown)))
+
+(provide 'test-wttrin--format-staleness-header)
+;;; test-wttrin--format-staleness-header.el ends here
diff --git a/tests/test-wttrin--get-cached-or-fetch.el b/tests/test-wttrin--get-cached-or-fetch.el
index 77a2689..e3383c6 100644
--- a/tests/test-wttrin--get-cached-or-fetch.el
+++ b/tests/test-wttrin--get-cached-or-fetch.el
@@ -5,7 +5,9 @@
;;; Commentary:
;; Unit tests for wttrin--get-cached-or-fetch function.
-;; Tests the core cache workflow: cache hits, misses, expiration, and error fallback.
+;; Tests the core cache workflow: cache hits, misses, and error fallback.
+;; TTL-based expiration has been removed — cached data is served regardless
+;; of age, with proactive refresh keeping data fresh in the background.
;;; Code:
@@ -36,7 +38,7 @@
;;; Normal Cases
(ert-deftest test-wttrin--get-cached-or-fetch-normal-cache-hit-returns-cached-data ()
- "Test that fresh cached data is returned without fetching."
+ "Test that cached data is returned without fetching."
(test-wttrin--get-cached-or-fetch-setup)
(unwind-protect
(let* ((location "Paris")
@@ -44,11 +46,11 @@
(now 1000.0)
(callback-result nil)
(fetch-called nil))
- ;; Pre-populate cache with fresh data
+ ;; Pre-populate cache with data
(puthash cache-key (cons now test-wttrin--get-cached-or-fetch-sample-weather)
wttrin--cache)
- ;; Mock time to be 100 seconds later (well within TTL of 900)
+ ;; Mock time to be 100 seconds later
(cl-letf (((symbol-function 'float-time)
(lambda () (+ now 100.0)))
((symbol-function 'wttrin-fetch-raw-string)
@@ -99,14 +101,15 @@
(should (equal (cdr cached) test-wttrin--get-cached-or-fetch-new-weather)))))
(test-wttrin--get-cached-or-fetch-teardown)))
-(ert-deftest test-wttrin--get-cached-or-fetch-normal-expired-cache-fetches-new-data ()
- "Test that expired cache triggers fetch and updates cache."
+(ert-deftest test-wttrin--get-cached-or-fetch-normal-old-data-still-served ()
+ "Test that old cached data is served without fetching (no TTL expiration).
+Proactive refresh keeps data fresh; on-demand reads always use cache."
(test-wttrin--get-cached-or-fetch-setup)
(unwind-protect
(let* ((location "Tokyo")
(cache-key (wttrin--make-cache-key location))
(old-time 1000.0)
- (new-time (+ old-time 1000.0)) ; 1000 seconds later (> 900 TTL)
+ (new-time (+ old-time 10000.0)) ; Very old data
(callback-result nil)
(fetch-called nil))
@@ -117,81 +120,6 @@
(cl-letf (((symbol-function 'float-time)
(lambda () new-time))
((symbol-function 'wttrin-fetch-raw-string)
- (lambda (_location callback)
- (setq fetch-called t)
- (funcall callback test-wttrin--get-cached-or-fetch-new-weather)))
- ((symbol-function 'wttrin--cleanup-cache-if-needed)
- (lambda () nil)))
-
- (wttrin--get-cached-or-fetch
- location
- (lambda (data) (setq callback-result data)))
-
- ;; Should call fetch due to expiration
- (should fetch-called)
- ;; Should return new data
- (should (equal callback-result test-wttrin--get-cached-or-fetch-new-weather))
- ;; Should update cache timestamp
- (let ((cached (gethash cache-key wttrin--cache)))
- (should (equal (car cached) new-time))
- (should (equal (cdr cached) test-wttrin--get-cached-or-fetch-new-weather)))))
- (test-wttrin--get-cached-or-fetch-teardown)))
-
-;;; Boundary Cases
-
-(ert-deftest test-wttrin--get-cached-or-fetch-boundary-exactly-at-ttl-fetches ()
- "Test that cache exactly at TTL boundary triggers fetch."
- (test-wttrin--get-cached-or-fetch-setup)
- (unwind-protect
- (let* ((location "Berlin")
- (cache-key (wttrin--make-cache-key location))
- (old-time 1000.0)
- ;; Exactly at TTL boundary (900 seconds = wttrin-cache-ttl)
- (new-time (+ old-time wttrin-cache-ttl))
- (callback-result nil)
- (fetch-called nil))
-
- ;; Pre-populate cache
- (puthash cache-key (cons old-time test-wttrin--get-cached-or-fetch-sample-weather)
- wttrin--cache)
-
- (cl-letf (((symbol-function 'float-time)
- (lambda () new-time))
- ((symbol-function 'wttrin-fetch-raw-string)
- (lambda (_location callback)
- (setq fetch-called t)
- (funcall callback test-wttrin--get-cached-or-fetch-new-weather)))
- ((symbol-function 'wttrin--cleanup-cache-if-needed)
- (lambda () nil)))
-
- (wttrin--get-cached-or-fetch
- location
- (lambda (data) (setq callback-result data)))
-
- ;; At exactly TTL, should fetch (not <)
- (should fetch-called)
- (should (equal callback-result test-wttrin--get-cached-or-fetch-new-weather))))
- (test-wttrin--get-cached-or-fetch-teardown)))
-
-(ert-deftest test-wttrin--get-cached-or-fetch-boundary-one-second-before-ttl-uses-cache ()
- "Test that cache one second before TTL uses cached data."
- (test-wttrin--get-cached-or-fetch-setup)
- (unwind-protect
- (let* ((location "Madrid")
- (cache-key (wttrin--make-cache-key location))
- (old-time 1000.0)
- ;; One second before TTL expiration
- (new-time (+ old-time (- wttrin-cache-ttl 1)))
- (callback-result nil)
- (fetch-called nil))
-
- ;; Pre-populate cache
- (puthash cache-key (cons old-time test-wttrin--get-cached-or-fetch-sample-weather)
- wttrin--cache)
-
- (cl-letf (((symbol-function 'float-time)
- (lambda () new-time))
- ((symbol-function 'wttrin-fetch-raw-string)
(lambda (_location _callback)
(setq fetch-called t))))
@@ -199,11 +127,13 @@
location
(lambda (data) (setq callback-result data)))
- ;; Should use cache (still fresh)
+ ;; Should serve old data without fetching
(should-not fetch-called)
(should (equal callback-result test-wttrin--get-cached-or-fetch-sample-weather))))
(test-wttrin--get-cached-or-fetch-teardown)))
+;;; Boundary Cases
+
(ert-deftest test-wttrin--get-cached-or-fetch-boundary-force-refresh-bypasses-fresh-cache ()
"Test that force refresh flag bypasses fresh cache."
(test-wttrin--get-cached-or-fetch-setup)
@@ -220,7 +150,7 @@
wttrin--cache)
(cl-letf (((symbol-function 'float-time)
- (lambda () (+ now 100.0))) ; Well within TTL
+ (lambda () (+ now 100.0)))
((symbol-function 'wttrin-fetch-raw-string)
(lambda (_location callback)
(setq fetch-called t)
@@ -314,17 +244,16 @@
(unwind-protect
(let* ((location "Vienna")
(cache-key (wttrin--make-cache-key location))
- (old-time 1000.0)
- (new-time (+ old-time 2000.0)) ; Well expired
(callback-result nil)
- (message-shown nil))
+ (message-shown nil)
+ (wttrin--force-refresh t)) ; Force refresh to trigger fetch
- ;; Pre-populate cache with expired data
- (puthash cache-key (cons old-time test-wttrin--get-cached-or-fetch-sample-weather)
+ ;; Pre-populate cache with data
+ (puthash cache-key (cons 1000.0 test-wttrin--get-cached-or-fetch-sample-weather)
wttrin--cache)
(cl-letf (((symbol-function 'float-time)
- (lambda () new-time))
+ (lambda () 3000.0))
((symbol-function 'wttrin-fetch-raw-string)
(lambda (_location callback)
;; Simulate network failure
diff --git a/tests/test-wttrin--mode-line-map.el b/tests/test-wttrin--mode-line-map.el
index 2328300..ef8f0d2 100644
--- a/tests/test-wttrin--mode-line-map.el
+++ b/tests/test-wttrin--mode-line-map.el
@@ -75,16 +75,17 @@
"Test that mode-line display uses wttrin--mode-line-map after refactoring.
This test verifies the refactoring eliminated inline keymap construction."
;; Set up minimal mode-line state
- (let ((wttrin-favorite-location "Test, CA")
- (wttrin--mode-line-tooltip-data "Test weather"))
- ;; Update the mode-line display
- (wttrin--mode-line-update-display "☀️")
+ (let ((wttrin-favorite-location "Test, CA"))
+ (cl-letf (((symbol-function 'float-time) (lambda () 1000.0)))
+ (setq wttrin--mode-line-cache (cons 1000.0 "Test, CA: ☀️ +61°F Clear"))
+ (wttrin--mode-line-update-display))
;; Extract the keymap property from wttrin-mode-line-string
(let ((keymap-prop (get-text-property 0 'local-map wttrin-mode-line-string)))
;; After refactoring, should use the shared wttrin--mode-line-map
;; Not a freshly constructed keymap on each call
- (should (eq keymap-prop wttrin--mode-line-map)))))
+ (should (eq keymap-prop wttrin--mode-line-map)))
+ (setq wttrin--mode-line-cache nil)))
(provide 'test-wttrin--mode-line-map)
;;; test-wttrin--mode-line-map.el ends here
diff --git a/tests/test-wttrin--mode-line-update-display.el b/tests/test-wttrin--mode-line-update-display.el
index 07ab73f..57a5823 100644
--- a/tests/test-wttrin--mode-line-update-display.el
+++ b/tests/test-wttrin--mode-line-update-display.el
@@ -3,8 +3,9 @@
;; Copyright (C) 2025 Craig Jennings
;;; Commentary:
-;; Unit tests for wttrin--mode-line-update-display and
-;; wttrin--mode-line-valid-response-p.
+;; Unit tests for wttrin--mode-line-update-display,
+;; wttrin--mode-line-valid-response-p, wttrin--mode-line-fetch-weather,
+;; wttrin--mode-line-set-placeholder, and wttrin--mode-line-update-placeholder-error.
;;; Code:
@@ -18,13 +19,13 @@
"Setup for mode-line update display tests."
(testutil-wttrin-setup)
(setq wttrin-mode-line-string nil)
- (setq wttrin--mode-line-tooltip-data nil))
+ (setq wttrin--mode-line-cache nil))
(defun test-wttrin--mode-line-update-display-teardown ()
"Teardown for mode-line update display tests."
(testutil-wttrin-teardown)
(setq wttrin-mode-line-string nil)
- (setq wttrin--mode-line-tooltip-data nil))
+ (setq wttrin--mode-line-cache nil))
;;; --------------------------------------------------------------------------
;;; wttrin--mode-line-valid-response-p
@@ -65,45 +66,65 @@
(should-not (wttrin--mode-line-valid-response-p 42)))
;;; --------------------------------------------------------------------------
+;;; wttrin--format-age
+;;; --------------------------------------------------------------------------
+
+(ert-deftest test-wttrin--format-age-just-now ()
+ "Seconds under 60 returns just now."
+ (should (equal (wttrin--format-age 0) "just now"))
+ (should (equal (wttrin--format-age 59) "just now")))
+
+(ert-deftest test-wttrin--format-age-minutes ()
+ "Seconds in the minutes range."
+ (should (equal (wttrin--format-age 60) "1 minute ago"))
+ (should (equal (wttrin--format-age 300) "5 minutes ago"))
+ (should (equal (wttrin--format-age 3599) "59 minutes ago")))
+
+(ert-deftest test-wttrin--format-age-hours ()
+ "Seconds in the hours range."
+ (should (equal (wttrin--format-age 3600) "1 hour ago"))
+ (should (equal (wttrin--format-age 7200) "2 hours ago"))
+ (should (equal (wttrin--format-age 86399) "23 hours ago")))
+
+(ert-deftest test-wttrin--format-age-days ()
+ "Seconds in the days range."
+ (should (equal (wttrin--format-age 86400) "1 day ago"))
+ (should (equal (wttrin--format-age 172800) "2 days ago")))
+
+;;; --------------------------------------------------------------------------
;;; wttrin--mode-line-update-display
;;; --------------------------------------------------------------------------
;;; Normal Cases
(ert-deftest test-wttrin--mode-line-update-display-normal-sets-mode-line-string ()
- "Display update sets wttrin-mode-line-string to non-nil."
+ "Display update from cache sets wttrin-mode-line-string to non-nil."
(test-wttrin--mode-line-update-display-setup)
(unwind-protect
- (progn
- (wttrin--mode-line-update-display "Paris: ☀️ +61°F Clear")
+ (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)
(should wttrin-mode-line-string))
(test-wttrin--mode-line-update-display-teardown)))
-(ert-deftest test-wttrin--mode-line-update-display-normal-stores-tooltip-data ()
- "Display update stores weather string as tooltip data."
- (test-wttrin--mode-line-update-display-setup)
- (unwind-protect
- (progn
- (wttrin--mode-line-update-display "Paris: ☀️ +61°F Clear")
- (should (equal wttrin--mode-line-tooltip-data "Paris: ☀️ +61°F Clear")))
- (test-wttrin--mode-line-update-display-teardown)))
-
(ert-deftest test-wttrin--mode-line-update-display-normal-extracts-emoji ()
"Display update extracts emoji character into mode-line string."
(test-wttrin--mode-line-update-display-setup)
(unwind-protect
(let ((wttrin-mode-line-emoji-font nil))
- (wttrin--mode-line-update-display "Paris: X +61°F Clear")
- ;; Mode-line string should contain the extracted character
- (should (string-match-p "X" (substring-no-properties wttrin-mode-line-string))))
+ (cl-letf (((symbol-function 'float-time) (lambda () 1000.0)))
+ (setq wttrin--mode-line-cache (cons 1000.0 "Paris: X +61°F Clear"))
+ (wttrin--mode-line-update-display)
+ (should (string-match-p "X" (substring-no-properties wttrin-mode-line-string)))))
(test-wttrin--mode-line-update-display-teardown)))
(ert-deftest test-wttrin--mode-line-update-display-normal-has-help-echo ()
"Display update sets help-echo property for tooltip."
(test-wttrin--mode-line-update-display-setup)
(unwind-protect
- (progn
- (wttrin--mode-line-update-display "Paris: ☀️ +61°F Clear")
+ (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)
(should (get-text-property 0 'help-echo wttrin-mode-line-string)))
(test-wttrin--mode-line-update-display-teardown)))
@@ -111,91 +132,124 @@
"Display update sets local-map property for mouse interaction."
(test-wttrin--mode-line-update-display-setup)
(unwind-protect
- (progn
- (wttrin--mode-line-update-display "Paris: ☀️ +61°F Clear")
+ (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)
(should (eq (get-text-property 0 'local-map wttrin-mode-line-string)
wttrin--mode-line-map)))
(test-wttrin--mode-line-update-display-teardown)))
-;;; Boundary Cases
-
-(ert-deftest test-wttrin--mode-line-update-display-boundary-no-emoji-match-uses-fallback ()
- "When emoji regex doesn't match, fallback character '?' is used."
+(ert-deftest test-wttrin--mode-line-update-display-normal-fresh-tooltip-shows-updated ()
+ "Fresh data tooltip shows weather data and 'Updated' age."
(test-wttrin--mode-line-update-display-setup)
(unwind-protect
- (let ((wttrin-mode-line-emoji-font nil))
- (wttrin--mode-line-update-display "no colon here")
- (should (string-match-p "\\?" (substring-no-properties wttrin-mode-line-string))))
+ (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)))
+ (should (string-match-p "Paris" tooltip))
+ (should (string-match-p "Updated 5 minutes ago" tooltip))))
(test-wttrin--mode-line-update-display-teardown)))
-;;; Tooltip Lambda Tests
+;;; Stale Cases
-(ert-deftest test-wttrin--mode-line-update-display-normal-tooltip-returns-weather-data ()
- "Tooltip lambda returns weather data when available."
+(ert-deftest test-wttrin--mode-line-update-display-stale-tooltip-shows-stale ()
+ "Stale data tooltip indicates staleness and retry info."
(test-wttrin--mode-line-update-display-setup)
(unwind-protect
- (progn
- (wttrin--mode-line-update-display "Paris: ☀️ +61°F Clear")
- (let ((tooltip-fn (get-text-property 0 'help-echo wttrin-mode-line-string)))
- (should (equal (funcall tooltip-fn nil nil nil) "Paris: ☀️ +61°F Clear"))))
+ (let ((wttrin-mode-line-refresh-interval 900))
+ (cl-letf (((symbol-function 'float-time) (lambda () 3000.0)))
+ ;; 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)))
+ (should (string-match-p "Stale" tooltip))
+ (should (string-match-p "fetch failed" tooltip)))))
(test-wttrin--mode-line-update-display-teardown)))
-(ert-deftest test-wttrin--mode-line-update-display-boundary-tooltip-empty-string-uses-fallback ()
- "Tooltip lambda falls back when tooltip data is empty string."
+(ert-deftest test-wttrin--mode-line-update-display-stale-emoji-dimmed ()
+ "Stale data dims the emoji with gray foreground."
(test-wttrin--mode-line-update-display-setup)
(unwind-protect
- (let ((wttrin-favorite-location "Paris"))
- (wttrin--mode-line-update-display "Paris: ☀️ +61°F Clear")
- ;; Simulate empty tooltip data (as would happen with bad response)
- (setq wttrin--mode-line-tooltip-data "")
- (let ((tooltip-fn (get-text-property 0 'help-echo wttrin-mode-line-string)))
- (should (string-match-p "Weather for Paris" (funcall tooltip-fn nil nil nil)))))
+ (let ((wttrin-mode-line-refresh-interval 900)
+ (wttrin-mode-line-emoji-font nil))
+ (cl-letf (((symbol-function 'float-time) (lambda () 3000.0)))
+ (setq wttrin--mode-line-cache (cons 1000.0 "Paris: X +61°F Clear"))
+ (wttrin--mode-line-update-display)
+ ;; The emoji character should have a gray face
+ (let* ((str wttrin-mode-line-string)
+ ;; Find the emoji position (after the space)
+ (emoji-pos 1)
+ (face (get-text-property emoji-pos 'face str)))
+ (should face)
+ (should (equal (plist-get face :foreground) "gray60")))))
(test-wttrin--mode-line-update-display-teardown)))
-(ert-deftest test-wttrin--mode-line-update-display-boundary-tooltip-nil-uses-fallback ()
- "Tooltip lambda falls back when tooltip data is nil."
+;;; Boundary Cases
+
+(ert-deftest test-wttrin--mode-line-update-display-boundary-no-emoji-match-uses-fallback ()
+ "When emoji regex doesn't match, fallback character '?' is used."
(test-wttrin--mode-line-update-display-setup)
(unwind-protect
- (let ((wttrin-favorite-location "Paris"))
- (wttrin--mode-line-update-display "Paris: ☀️ +61°F Clear")
- (setq wttrin--mode-line-tooltip-data nil)
- (let ((tooltip-fn (get-text-property 0 'help-echo wttrin-mode-line-string)))
- (should (string-match-p "Weather for Paris" (funcall tooltip-fn nil nil nil)))))
+ (let ((wttrin-mode-line-emoji-font nil))
+ (cl-letf (((symbol-function 'float-time) (lambda () 1000.0)))
+ (setq wttrin--mode-line-cache (cons 1000.0 "no colon here"))
+ (wttrin--mode-line-update-display)
+ (should (string-match-p "\\?" (substring-no-properties wttrin-mode-line-string)))))
+ (test-wttrin--mode-line-update-display-teardown)))
+
+(ert-deftest test-wttrin--mode-line-update-display-boundary-nil-cache-does-nothing ()
+ "When cache is nil, update-display does not set mode-line-string."
+ (test-wttrin--mode-line-update-display-setup)
+ (unwind-protect
+ (progn
+ (setq wttrin--mode-line-cache nil)
+ (wttrin--mode-line-update-display)
+ (should-not wttrin-mode-line-string))
(test-wttrin--mode-line-update-display-teardown)))
;;; --------------------------------------------------------------------------
-;;; wttrin--mode-line-fetch-weather (validation integration)
+;;; wttrin--mode-line-fetch-weather
;;; --------------------------------------------------------------------------
+(ert-deftest test-wttrin--mode-line-fetch-weather-normal-valid-response-updates-cache ()
+ "Valid API response populates cache and updates display."
+ (test-wttrin--mode-line-update-display-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris"))
+ (testutil-wttrin-mock-http-response "Paris: ☀️ +61°F Clear"
+ (wttrin--mode-line-fetch-weather)
+ (should wttrin--mode-line-cache)
+ (should (equal (cdr wttrin--mode-line-cache) "Paris: ☀️ +61°F Clear"))
+ (should wttrin-mode-line-string)))
+ (test-wttrin--mode-line-update-display-teardown)))
+
(ert-deftest test-wttrin--mode-line-fetch-weather-error-empty-response-keeps-previous ()
- "Empty API response does not overwrite previous valid display."
+ "Empty API response does not overwrite previous valid cache."
(test-wttrin--mode-line-update-display-setup)
(unwind-protect
(let ((wttrin-favorite-location "Paris"))
- ;; Set up a valid prior state
- (wttrin--mode-line-update-display "Paris: ☀️ +61°F Clear")
- (let ((previous-string wttrin-mode-line-string)
- (previous-tooltip wttrin--mode-line-tooltip-data))
+ ;; Set up a valid prior cache
+ (setq wttrin--mode-line-cache (cons (float-time) "Paris: ☀️ +61°F Clear"))
+ (wttrin--mode-line-update-display)
+ (let ((previous-cache wttrin--mode-line-cache))
;; Simulate fetch returning empty response
(testutil-wttrin-mock-http-response ""
(wttrin--mode-line-fetch-weather)
- ;; Previous state should be preserved
- (should (equal wttrin-mode-line-string previous-string))
- (should (equal wttrin--mode-line-tooltip-data previous-tooltip)))))
+ ;; Cache should be preserved
+ (should (equal wttrin--mode-line-cache previous-cache)))))
(test-wttrin--mode-line-update-display-teardown)))
(ert-deftest test-wttrin--mode-line-fetch-weather-error-no-colon-response-keeps-previous ()
- "Malformed API response without colon does not overwrite previous valid display."
+ "Malformed API response without colon does not overwrite previous valid cache."
(test-wttrin--mode-line-update-display-setup)
(unwind-protect
(let ((wttrin-favorite-location "Paris"))
- (wttrin--mode-line-update-display "Paris: ☀️ +61°F Clear")
- (let ((previous-string wttrin-mode-line-string)
- (previous-tooltip wttrin--mode-line-tooltip-data))
+ (setq wttrin--mode-line-cache (cons (float-time) "Paris: ☀️ +61°F Clear"))
+ (let ((previous-cache wttrin--mode-line-cache))
(testutil-wttrin-mock-http-response "Unknown location"
(wttrin--mode-line-fetch-weather)
- (should (equal wttrin-mode-line-string previous-string))
- (should (equal wttrin--mode-line-tooltip-data previous-tooltip)))))
+ (should (equal wttrin--mode-line-cache previous-cache)))))
(test-wttrin--mode-line-update-display-teardown)))
(ert-deftest test-wttrin--mode-line-fetch-weather-error-nil-location-does-not-fetch ()
@@ -210,15 +264,43 @@
(should-not fetch-called)))
(test-wttrin--mode-line-update-display-teardown)))
-(ert-deftest test-wttrin--mode-line-fetch-weather-normal-valid-response-updates-display ()
- "Valid API response updates the mode-line display."
+(ert-deftest test-wttrin--mode-line-fetch-weather-error-network-fail-with-cache-shows-stale ()
+ "Network failure with existing cache triggers stale display."
(test-wttrin--mode-line-update-display-setup)
(unwind-protect
- (let ((wttrin-favorite-location "Paris"))
- (testutil-wttrin-mock-http-response "Paris: ☀️ +61°F Clear"
+ (let ((wttrin-favorite-location "Paris")
+ (wttrin-mode-line-refresh-interval 900))
+ ;; Set up old cache data
+ (setq wttrin--mode-line-cache (cons (- (float-time) 2000) "Paris: ☀️ +61°F Clear"))
+ ;; Simulate network failure (nil data)
+ (cl-letf (((symbol-function 'wttrin--fetch-url)
+ (lambda (_url callback) (funcall callback nil))))
(wttrin--mode-line-fetch-weather)
+ ;; Cache should still exist
+ (should wttrin--mode-line-cache)
+ ;; Mode-line should be updated (stale display)
+ (should wttrin-mode-line-string)))
+ (test-wttrin--mode-line-update-display-teardown)))
+
+(ert-deftest test-wttrin--mode-line-fetch-weather-error-network-fail-no-cache-shows-error ()
+ "Network failure with no cache shows error placeholder."
+ (test-wttrin--mode-line-update-display-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris")
+ (wttrin-mode-line-emoji-font nil))
+ ;; No cache
+ (should-not wttrin--mode-line-cache)
+ ;; Simulate network failure
+ (cl-letf (((symbol-function 'wttrin--fetch-url)
+ (lambda (_url callback) (funcall callback nil))))
+ (wttrin--mode-line-fetch-weather)
+ ;; Should show error placeholder with hourglass
(should wttrin-mode-line-string)
- (should (equal wttrin--mode-line-tooltip-data "Paris: ☀️ +61°F Clear"))))
+ (should (string-match-p "⏳" (substring-no-properties wttrin-mode-line-string)))
+ ;; Tooltip should mention failure
+ (let ((tooltip (get-text-property 0 'help-echo wttrin-mode-line-string)))
+ (should (string-match-p "failed" tooltip))
+ (should (string-match-p "Paris" tooltip)))))
(test-wttrin--mode-line-update-display-teardown)))
;;; --------------------------------------------------------------------------
@@ -268,26 +350,65 @@
(test-wttrin--mode-line-update-display-teardown)))
(ert-deftest test-wttrin--mode-line-set-placeholder-normal-replaced-by-real-data ()
- "Placeholder is replaced when real weather data arrives."
+ "Placeholder is replaced when real weather data arrives via cache."
(test-wttrin--mode-line-update-display-setup)
(unwind-protect
(let ((wttrin-favorite-location "Paris")
(wttrin-mode-line-emoji-font nil))
(wttrin--mode-line-set-placeholder)
(should (string-match-p "⏳" (substring-no-properties wttrin-mode-line-string)))
- (wttrin--mode-line-update-display "Paris: ☀️ +61°F Clear")
+ (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))
(should-not (string-match-p "⏳" (substring-no-properties wttrin-mode-line-string))))
(test-wttrin--mode-line-update-display-teardown)))
;;; Boundary Cases
-(ert-deftest test-wttrin--mode-line-set-placeholder-boundary-does-not-set-tooltip-data ()
- "Placeholder does not contaminate tooltip data variable."
+(ert-deftest test-wttrin--mode-line-set-placeholder-boundary-does-not-set-cache ()
+ "Placeholder does not contaminate cache variable."
(test-wttrin--mode-line-update-display-setup)
(unwind-protect
(let ((wttrin-favorite-location "Paris"))
(wttrin--mode-line-set-placeholder)
- (should-not wttrin--mode-line-tooltip-data))
+ (should-not wttrin--mode-line-cache))
+ (test-wttrin--mode-line-update-display-teardown)))
+
+;;; --------------------------------------------------------------------------
+;;; wttrin--mode-line-update-placeholder-error
+;;; --------------------------------------------------------------------------
+
+(ert-deftest test-wttrin--mode-line-update-placeholder-error-sets-mode-line-string ()
+ "Error placeholder sets wttrin-mode-line-string to non-nil."
+ (test-wttrin--mode-line-update-display-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris"))
+ (wttrin--mode-line-update-placeholder-error)
+ (should wttrin-mode-line-string))
+ (test-wttrin--mode-line-update-display-teardown)))
+
+(ert-deftest test-wttrin--mode-line-update-placeholder-error-contains-hourglass ()
+ "Error placeholder displays hourglass emoji."
+ (test-wttrin--mode-line-update-display-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris")
+ (wttrin-mode-line-emoji-font nil))
+ (wttrin--mode-line-update-placeholder-error)
+ (should (string-match-p "⏳" (substring-no-properties wttrin-mode-line-string))))
+ (test-wttrin--mode-line-update-display-teardown)))
+
+(ert-deftest test-wttrin--mode-line-update-placeholder-error-tooltip-mentions-failure ()
+ "Error placeholder tooltip mentions failure and retry."
+ (test-wttrin--mode-line-update-display-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris")
+ (wttrin-mode-line-refresh-interval 900))
+ (wttrin--mode-line-update-placeholder-error)
+ (let ((tooltip (get-text-property 0 'help-echo wttrin-mode-line-string)))
+ (should (string-match-p "failed" tooltip))
+ (should (string-match-p "Paris" tooltip))
+ (should (string-match-p "retry" tooltip))
+ (should (string-match-p "15 minutes" tooltip))))
(test-wttrin--mode-line-update-display-teardown)))
(provide 'test-wttrin--mode-line-update-display)
diff --git a/tests/test-wttrin-smoke.el b/tests/test-wttrin-smoke.el
index 2a2d828..1fa0539 100644
--- a/tests/test-wttrin-smoke.el
+++ b/tests/test-wttrin-smoke.el
@@ -80,7 +80,7 @@ This is a REQUIRED dependency - wttrin cannot function without it."
"Test that key defcustom variables are defined."
(should (boundp 'wttrin-default-locations))
(should (boundp 'wttrin-unit-system))
- (should (boundp 'wttrin-cache-ttl))
+ (should (boundp 'wttrin-refresh-interval))
(should (boundp 'wttrin-cache-max-entries))
(should (boundp 'wttrin-favorite-location))
(should (boundp 'wttrin-mode-line-refresh-interval))
diff --git a/tests/testutil-wttrin.el b/tests/testutil-wttrin.el
index 685cc09..42c8c21 100644
--- a/tests/testutil-wttrin.el
+++ b/tests/testutil-wttrin.el
@@ -72,10 +72,10 @@
`(let ((wttrin-unit-system ,unit-system))
,@body))
-(defmacro testutil-wttrin-with-cache-ttl (ttl &rest body)
- "Execute BODY with wttrin-cache-ttl temporarily set to TTL."
+(defmacro testutil-wttrin-with-refresh-interval (interval &rest body)
+ "Execute BODY with wttrin-refresh-interval temporarily set to INTERVAL."
(declare (indent 1))
- `(let ((wttrin-cache-ttl ,ttl))
+ `(let ((wttrin-refresh-interval ,interval))
,@body))
(defmacro testutil-wttrin-with-cache-max (max-entries &rest body)
@@ -110,6 +110,15 @@
(funcall callback nil)))))
,@body))
+;;; Mode-line Cache Helpers
+
+(defun testutil-wttrin-set-mode-line-cache (data &optional age-seconds)
+ "Set mode-line cache to DATA, optionally aged by AGE-SECONDS."
+ (let ((timestamp (if age-seconds
+ (- (float-time) age-seconds)
+ (float-time))))
+ (setq wttrin--mode-line-cache (cons timestamp data))))
+
;;; Test Setup and Teardown
(defun testutil-wttrin-setup ()