diff options
| author | Craig Jennings <c@cjennings.net> | 2026-02-21 07:06:50 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-02-21 07:11:03 -0600 |
| commit | b74b98f177d92d50ddbede900ba41212e07c5f63 (patch) | |
| tree | 459b1630dcc7d1c941f850565acdc16332831948 /tests | |
| parent | ec8130cfe1a7390e9939b311c8db39907a3f7f44 (diff) | |
| download | emacs-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.el | 8 | ||||
| -rw-r--r-- | tests/test-wttrin--format-staleness-header.el | 80 | ||||
| -rw-r--r-- | tests/test-wttrin--get-cached-or-fetch.el | 109 | ||||
| -rw-r--r-- | tests/test-wttrin--mode-line-map.el | 11 | ||||
| -rw-r--r-- | tests/test-wttrin--mode-line-update-display.el | 275 | ||||
| -rw-r--r-- | tests/test-wttrin-smoke.el | 2 | ||||
| -rw-r--r-- | tests/testutil-wttrin.el | 15 |
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 () |
