aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-05 10:06:20 -0500
committerCraig Jennings <c@cjennings.net>2026-05-05 10:06:20 -0500
commit541586e3ff0606afcf0581210a62c95f5ee65c71 (patch)
treed67d1f08323879e72a10b42f945edff9004675a8 /tests
parentec00dbe1c03527ec46a0faa20545a7acd382da84 (diff)
downloademacs-wttrin-541586e3ff0606afcf0581210a62c95f5ee65c71.tar.gz
emacs-wttrin-541586e3ff0606afcf0581210a62c95f5ee65c71.zip
feat: support t for auto-detect via geolocation in wttrin-favorite-location
Completes the three-mode configuration the favorite-location feature was always meant to have: - nil — disabled (default; unchanged) - a string — explicit location (unchanged) - t — auto-detect via IP geolocation (NEW) When the user sets `wttrin-favorite-location` to t, wttrin runs the geolocation lookup once on first use and caches the result for the session. Subsequent reads return the cached string. The lookup happens in the background via the existing `wttrin-geolocation-detect`, so Emacs startup is never blocked. I added two private state vars (`wttrin--resolved-favorite-location`, `wttrin--favorite-location-pending`) and a resolver `wttrin--resolve-favorite-location` that maps the three modes onto a returned string or nil. When t is set and the cache is empty, the resolver kicks off the lookup and returns nil for that call — the next consumer tick after the callback completes gets the cached string. The pending flag prevents duplicate concurrent lookups when several consumers ask during the resolution window. Five consumer call sites now go through the resolver instead of reading `wttrin-favorite-location` directly: `wttrin--mode-line-fetch-weather`, `wttrin-mode-line-click`, `wttrin-mode-line-force-refresh`, `wttrin--buffer-cache-refresh`, and `wttrin--mode-line-start`. Two display sites (the placeholder and error tooltips) use a new `wttrin--favorite-location-display-name` helper that returns "current location" while a t-mode lookup is pending, instead of showing the literal `t` to the user. Tests cover the resolver across all three modes, including the pending state, the duplicate-suppression behavior, and detection-failure retry. Existing consumer tests stay green because the resolver returns the bound string unchanged when the variable is a string. One care: the test file requires wttrin-geolocation up front so cl-letf mocks of `wttrin-geolocation-detect` aren't undone by the resolver's lazy require — without that, the first run hit ipapi.co for real. README documents the new mode under "Setting the Favorite Location from IP Geolocation".
Diffstat (limited to 'tests')
-rw-r--r--tests/test-wttrin--resolve-favorite-location.el154
1 files changed, 154 insertions, 0 deletions
diff --git a/tests/test-wttrin--resolve-favorite-location.el b/tests/test-wttrin--resolve-favorite-location.el
new file mode 100644
index 0000000..506c226
--- /dev/null
+++ b/tests/test-wttrin--resolve-favorite-location.el
@@ -0,0 +1,154 @@
+;;; test-wttrin--resolve-favorite-location.el --- Tests for wttrin--resolve-favorite-location -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for `wttrin--resolve-favorite-location' and its helpers.
+;; The resolver maps the three modes of `wttrin-favorite-location'
+;; (nil / string / t) onto a returned location string, kicking off an
+;; async geolocation lookup the first time t is seen.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;; Load wttrin-geolocation up front so cl-letf mocks of
+;; `wttrin-geolocation-detect' aren't undone by the resolver's lazy
+;; (require 'wttrin-geolocation) — which would otherwise re-evaluate
+;; the defun and overwrite the mocked symbol-function.
+(require 'wttrin-geolocation
+ (expand-file-name "wttrin-geolocation.el"
+ (file-name-directory (locate-library "wttrin"))))
+
+;;; Helpers
+
+(defmacro test-wttrin--resolve-with-geolocation-mock (returned-location &rest body)
+ "Run BODY with `wttrin-geolocation-detect' mocked to call back with
+RETURNED-LOCATION synchronously. Resets resolver caches before BODY."
+ (declare (indent 1))
+ `(let ((wttrin--resolved-favorite-location nil)
+ (wttrin--favorite-location-pending nil))
+ (cl-letf (((symbol-function 'wttrin-geolocation-detect)
+ (lambda (callback) (funcall callback ,returned-location))))
+ ,@body)))
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin--resolve-favorite-location-normal-nil-returns-nil ()
+ "Disabled mode (nil) resolves to nil."
+ (let ((wttrin-favorite-location nil)
+ (wttrin--resolved-favorite-location nil)
+ (wttrin--favorite-location-pending nil))
+ (should-not (wttrin--resolve-favorite-location))))
+
+(ert-deftest test-wttrin--resolve-favorite-location-normal-string-returns-itself ()
+ "Explicit string resolves to itself."
+ (let ((wttrin-favorite-location "Berkeley, CA")
+ (wttrin--resolved-favorite-location nil)
+ (wttrin--favorite-location-pending nil))
+ (should (string= "Berkeley, CA" (wttrin--resolve-favorite-location)))))
+
+(ert-deftest test-wttrin--resolve-favorite-location-normal-t-with-cache-returns-cached ()
+ "When t and cache populated, resolver returns cached value without re-fetching."
+ (let ((wttrin-favorite-location t)
+ (wttrin--resolved-favorite-location "Cached, Place")
+ (wttrin--favorite-location-pending nil)
+ (detect-calls 0))
+ (cl-letf (((symbol-function 'wttrin-geolocation-detect)
+ (lambda (_callback) (cl-incf detect-calls))))
+ (should (string= "Cached, Place" (wttrin--resolve-favorite-location)))
+ (should (= 0 detect-calls)))))
+
+(ert-deftest test-wttrin--resolve-favorite-location-normal-t-detection-success-caches-result ()
+ "t-mode detect callback populates the resolver cache."
+ (let ((wttrin-favorite-location t))
+ (test-wttrin--resolve-with-geolocation-mock "Detected, City"
+ (wttrin--resolve-favorite-location)
+ (should (string= "Detected, City" wttrin--resolved-favorite-location))
+ (should-not wttrin--favorite-location-pending))))
+
+(ert-deftest test-wttrin--resolve-favorite-location-normal-second-call-returns-cached-from-first ()
+ "After detection completes, the second call returns the cached string."
+ (let ((wttrin-favorite-location t))
+ (test-wttrin--resolve-with-geolocation-mock "Detected, City"
+ (wttrin--resolve-favorite-location)
+ (should (string= "Detected, City" (wttrin--resolve-favorite-location))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin--resolve-favorite-location-boundary-t-without-cache-returns-nil ()
+ "First call with t and empty cache returns nil while detection runs."
+ (let ((wttrin-favorite-location t)
+ (wttrin--resolved-favorite-location nil)
+ (wttrin--favorite-location-pending nil))
+ (cl-letf (((symbol-function 'wttrin-geolocation-detect)
+ (lambda (_callback)
+ ;; Simulate an async lookup that has NOT called back yet.
+ (setq wttrin--favorite-location-pending t))))
+ (should-not (wttrin--resolve-favorite-location)))))
+
+(ert-deftest test-wttrin--resolve-favorite-location-boundary-pending-suppresses-duplicate-detect ()
+ "When a lookup is already pending, the resolver does not start another."
+ (let ((wttrin-favorite-location t)
+ (wttrin--resolved-favorite-location nil)
+ (wttrin--favorite-location-pending t)
+ (detect-calls 0))
+ (cl-letf (((symbol-function 'wttrin-geolocation-detect)
+ (lambda (_callback) (cl-incf detect-calls))))
+ (wttrin--resolve-favorite-location)
+ (should (= 0 detect-calls)))))
+
+;;; Error Cases
+
+(ert-deftest test-wttrin--resolve-favorite-location-error-detection-failure-leaves-cache-empty ()
+ "When the detect callback returns nil, the cache stays empty and pending clears."
+ (let ((wttrin-favorite-location t))
+ (test-wttrin--resolve-with-geolocation-mock nil
+ (wttrin--resolve-favorite-location)
+ (should-not wttrin--resolved-favorite-location)
+ (should-not wttrin--favorite-location-pending))))
+
+(ert-deftest test-wttrin--resolve-favorite-location-error-detection-failure-allows-retry ()
+ "After a failed detection, a subsequent resolve call kicks off a new lookup."
+ (let ((wttrin-favorite-location t)
+ (wttrin--resolved-favorite-location nil)
+ (wttrin--favorite-location-pending nil)
+ (detect-calls 0))
+ (cl-letf (((symbol-function 'wttrin-geolocation-detect)
+ (lambda (callback)
+ (cl-incf detect-calls)
+ (funcall callback nil))))
+ (wttrin--resolve-favorite-location)
+ (wttrin--resolve-favorite-location)
+ (should (= 2 detect-calls)))))
+
+;;; Display Name Helper
+
+(ert-deftest test-wttrin--favorite-location-display-name-resolved-string-returns-string ()
+ "Display name returns the resolved string when available."
+ (let ((wttrin-favorite-location "Tokyo, JP")
+ (wttrin--resolved-favorite-location nil)
+ (wttrin--favorite-location-pending nil))
+ (should (string= "Tokyo, JP" (wttrin--favorite-location-display-name)))))
+
+(ert-deftest test-wttrin--favorite-location-display-name-t-pending-returns-current-location-label ()
+ "Display name returns \"current location\" when t-mode lookup is pending."
+ (let ((wttrin-favorite-location t)
+ (wttrin--resolved-favorite-location nil)
+ (wttrin--favorite-location-pending t))
+ (should (string= "current location"
+ (wttrin--favorite-location-display-name)))))
+
+(ert-deftest test-wttrin--favorite-location-display-name-nil-returns-nil ()
+ "Display name returns nil when feature is disabled."
+ (let ((wttrin-favorite-location nil)
+ (wttrin--resolved-favorite-location nil)
+ (wttrin--favorite-location-pending nil))
+ (should-not (wttrin--favorite-location-display-name))))
+
+(provide 'test-wttrin--resolve-favorite-location)
+;;; test-wttrin--resolve-favorite-location.el ends here