aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test-wttrin--add-buffer-instructions.el34
-rw-r--r--tests/test-wttrin--display-weather.el15
-rw-r--r--tests/test-wttrin--mode-line-update-display.el24
-rw-r--r--tests/test-wttrin-geolocation-sentinel.el4
-rw-r--r--tests/test-wttrin-requery-force.el6
-rw-r--r--tests/test-wttrin-requery.el14
-rw-r--r--tests/test-wttrin-saved-locations.el360
-rw-r--r--tests/test-wttrin-set-location-from-geolocation.el23
-rw-r--r--tests/test-wttrin-use-current-location.el21
9 files changed, 471 insertions, 30 deletions
diff --git a/tests/test-wttrin--add-buffer-instructions.el b/tests/test-wttrin--add-buffer-instructions.el
index 425832d..fdd7c91 100644
--- a/tests/test-wttrin--add-buffer-instructions.el
+++ b/tests/test-wttrin--add-buffer-instructions.el
@@ -14,6 +14,17 @@
;;; Setup and Teardown
+(defun test-wttrin--expected-footer ()
+ "Return the footer string `wttrin--add-buffer-instructions' should produce.
+Width 23 is pinned here as a literal, independent of the production
+constant, so the test fails if the visible layout drifts."
+ (concat "\n\n"
+ (format "%-23s%s" "This view" "Saved locations") "\n"
+ (format "%-23s%s" "[a] another" "[s] save") "\n"
+ (format "%-23s%s" "[g] refresh" "[d] make default") "\n"
+ (format "%-23s%s" "[q] quit" "[r] rename") "\n"
+ (format "%-23s%s" "" "[x] remove")))
+
(defun test-wttrin--add-buffer-instructions-setup ()
"Setup for add-buffer-instructions tests."
(testutil-wttrin-setup))
@@ -28,7 +39,7 @@
"Test adding instructions to empty buffer."
(with-temp-buffer
(wttrin--add-buffer-instructions)
- (should (string= "\n\nPress: [a] for another location [g] to refresh [d] to make default [q] to quit"
+ (should (string= (test-wttrin--expected-footer)
(buffer-string)))))
(ert-deftest test-wttrin--add-buffer-instructions-normal-with-existing-content-appends-instructions ()
@@ -36,7 +47,8 @@
(with-temp-buffer
(insert "Weather: Sunny\nTemperature: 20°C")
(wttrin--add-buffer-instructions)
- (should (string= "Weather: Sunny\nTemperature: 20°C\n\nPress: [a] for another location [g] to refresh [d] to make default [q] to quit"
+ (should (string= (concat "Weather: Sunny\nTemperature: 20°C"
+ (test-wttrin--expected-footer))
(buffer-string)))))
(ert-deftest test-wttrin--add-buffer-instructions-normal-preserves-point-moves-to-end ()
@@ -56,12 +68,12 @@
(wttrin--add-buffer-instructions)
;; Should add instructions again, not check if they already exist
(should-not (string= first-result (buffer-string)))
- ;; Check for two occurrences of "Press:" by counting matches
+ ;; Check for two occurrences of the header by counting matches
(let ((count 0))
(with-temp-buffer
(insert first-result)
(goto-char (point-min))
- (while (search-forward "Press:" nil t)
+ (while (search-forward "This view" nil t)
(setq count (1+ count))))
;; After first call, should have 1 occurrence
(should (= 1 count)))
@@ -69,7 +81,7 @@
(let ((count 0))
(save-excursion
(goto-char (point-min))
- (while (search-forward "Press:" nil t)
+ (while (search-forward "This view" nil t)
(setq count (1+ count))))
(should (= 2 count))))))
@@ -88,7 +100,7 @@
(with-temp-buffer
(wttrin--add-buffer-instructions)
(goto-char (point-min))
- (search-forward "Press:")
+ (search-forward "another")
(should (eq (get-text-property (1- (point)) 'face) 'wttrin-instructions))))
;;; Boundary Cases
@@ -99,7 +111,7 @@
(insert "Weather data here")
(goto-char (point-min))
(wttrin--add-buffer-instructions)
- (should (string-suffix-p "Press: [a] for another location [g] to refresh [d] to make default [q] to quit"
+ (should (string-suffix-p "[x] remove"
(buffer-string)))))
(ert-deftest test-wttrin--add-buffer-instructions-boundary-point-in-middle-appends-at-end ()
@@ -109,7 +121,7 @@
(goto-char (point-min))
(forward-line 1)
(wttrin--add-buffer-instructions)
- (should (string-suffix-p "Press: [a] for another location [g] to refresh [d] to make default [q] to quit"
+ (should (string-suffix-p "[x] remove"
(buffer-string)))))
(ert-deftest test-wttrin--add-buffer-instructions-boundary-trailing-newlines-preserves-newlines ()
@@ -117,7 +129,7 @@
(with-temp-buffer
(insert "Weather\n\n\n")
(wttrin--add-buffer-instructions)
- (should (string= "Weather\n\n\n\n\nPress: [a] for another location [g] to refresh [d] to make default [q] to quit"
+ (should (string= (concat "Weather\n\n\n" (test-wttrin--expected-footer))
(buffer-string)))))
(ert-deftest test-wttrin--add-buffer-instructions-boundary-very-large-buffer-appends-at-end ()
@@ -126,7 +138,7 @@
(insert (make-string 10000 ?x))
(wttrin--add-buffer-instructions)
(goto-char (point-max))
- (should (looking-back "Press: \\[a\\] for another location \\[g\\] to refresh \\[d\\] to make default \\[q\\] to quit" nil))))
+ (should (looking-back "\\[x\\] remove" nil))))
;;; Error Cases
@@ -152,7 +164,7 @@
(widen)
(goto-char start)
(forward-line 1)
- (should (looking-at-p "\n\nPress:")))))
+ (should (looking-at-p "\n\nThis view")))))
(provide 'test-wttrin--add-buffer-instructions)
;;; test-wttrin--add-buffer-instructions.el ends here
diff --git a/tests/test-wttrin--display-weather.el b/tests/test-wttrin--display-weather.el
index 4cc4269..99ea067 100644
--- a/tests/test-wttrin--display-weather.el
+++ b/tests/test-wttrin--display-weather.el
@@ -97,13 +97,14 @@ Weather report: Paris, France
(wttrin--display-weather "Tokyo" test-wttrin--display-weather-sample-raw-data)
(with-current-buffer "*wttr.in*"
- (goto-char (point-max))
- (forward-line -2)
- ;; Should contain help text
- (should (search-forward "Press:" nil t))
- (should (search-forward "[a] for another location" nil t))
- (should (search-forward "[g] to refresh" nil t))
- (should (search-forward "[q] to quit" nil t))))
+ ;; The two-column footer carries both groups; check a token from
+ ;; each column independently (document order mixes them).
+ (let ((text (buffer-string)))
+ (should (string-match-p "This view" text))
+ (should (string-match-p "Saved locations" text))
+ (should (string-match-p "\\[a\\] another" text))
+ (should (string-match-p "\\[s\\] save" text))
+ (should (string-match-p "\\[x\\] remove" text)))))
(test-wttrin--display-weather-teardown)))
;;; Boundary Cases
diff --git a/tests/test-wttrin--mode-line-update-display.el b/tests/test-wttrin--mode-line-update-display.el
index 721517e..be066e7 100644
--- a/tests/test-wttrin--mode-line-update-display.el
+++ b/tests/test-wttrin--mode-line-update-display.el
@@ -252,6 +252,30 @@ trigger an emoji re-render so dimming matches the tooltip's staleness state."
(should wttrin-mode-line-string)))
(test-wttrin--mode-line-update-display-teardown)))
+(ert-deftest test-wttrin--mode-line-fetch-weather-normal-saved-name-shown-in-cache ()
+ "Normal: a saved-name favorite caches its display name, not the resolved query.
+The query (coordinates or an address) drives the fetch, but the hover tooltip
+should read the friendly name the user gave the place."
+ (test-wttrin--mode-line-update-display-setup)
+ (unwind-protect
+ (let ((wttrin-saved-locations '(("Mom's House" . "40.71,-74.01")))
+ (wttrin-favorite-location "Mom's House"))
+ (testutil-wttrin-mock-http-response "40.71,-74.01: ☀️ +70°F Cloudy"
+ (wttrin--mode-line-fetch-weather)
+ (should (string-prefix-p "Mom's House:" (cdr wttrin--mode-line-cache)))))
+ (test-wttrin--mode-line-update-display-teardown)))
+
+(ert-deftest test-wttrin--mode-line-fetch-weather-boundary-plain-location-unchanged ()
+ "Boundary: a plain (non-saved) favorite still caches its own name as the prefix."
+ (test-wttrin--mode-line-update-display-setup)
+ (unwind-protect
+ (let ((wttrin-saved-locations nil)
+ (wttrin-favorite-location "Paris"))
+ (testutil-wttrin-mock-http-response "Paris: ☀️ +61°F Clear"
+ (wttrin--mode-line-fetch-weather)
+ (should (string-prefix-p "Paris:" (cdr wttrin--mode-line-cache)))))
+ (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 cache."
(test-wttrin--mode-line-update-display-setup)
diff --git a/tests/test-wttrin-geolocation-sentinel.el b/tests/test-wttrin-geolocation-sentinel.el
index 61d8997..169761b 100644
--- a/tests/test-wttrin-geolocation-sentinel.el
+++ b/tests/test-wttrin-geolocation-sentinel.el
@@ -102,7 +102,7 @@ through `wttrin--query-selection' (smoke test of the entry wrapper)."
(unwind-protect
(let ((captured nil))
(cl-letf (((symbol-function 'wttrin-query)
- (lambda (loc) (setq captured loc))))
+ (lambda (loc &rest _) (setq captured loc))))
(wttrin--query-selection "Paris"))
(should (equal "Paris" captured)))
(testutil-wttrin-teardown)))
@@ -115,7 +115,7 @@ through `wttrin--query-selection' (smoke test of the entry wrapper)."
(cl-letf (((symbol-function 'wttrin-geolocation-detect)
(lambda (callback) (funcall callback "Austin, TX")))
((symbol-function 'wttrin-query)
- (lambda (loc &optional _address) (setq captured loc)))
+ (lambda (loc &rest _) (setq captured loc)))
((symbol-function 'message) (lambda (&rest _) nil)))
(wttrin--query-selection wttrin--geolocation-sentinel))
(should (equal "Austin, TX" captured)))
diff --git a/tests/test-wttrin-requery-force.el b/tests/test-wttrin-requery-force.el
index 171166d..30af7dd 100644
--- a/tests/test-wttrin-requery-force.el
+++ b/tests/test-wttrin-requery-force.el
@@ -35,7 +35,7 @@
(unwind-protect
(let ((queried-location nil))
(cl-letf (((symbol-function 'wttrin-query)
- (lambda (location) (setq queried-location location))))
+ (lambda (location &rest _) (setq queried-location location))))
;; Set up a weather buffer with a known location
(with-current-buffer (get-buffer-create "*wttr.in*")
(setq-local wttrin--current-location "Berlin, DE")
@@ -49,7 +49,7 @@
(unwind-protect
(let ((force-refresh-was-set nil))
(cl-letf (((symbol-function 'wttrin-query)
- (lambda (_location)
+ (lambda (_location &rest _)
(setq force-refresh-was-set wttrin--force-refresh))))
(with-current-buffer (get-buffer-create "*wttr.in*")
(setq-local wttrin--current-location "Paris")
@@ -76,7 +76,7 @@
(test-wttrin-requery-force-setup)
(unwind-protect
(progn
- (cl-letf (((symbol-function 'wttrin-query) (lambda (_location) nil)))
+ (cl-letf (((symbol-function 'wttrin-query) (lambda (_location &rest _) nil)))
(with-current-buffer (get-buffer-create "*wttr.in*")
(setq-local wttrin--current-location "Paris")
(wttrin-requery-force)))
diff --git a/tests/test-wttrin-requery.el b/tests/test-wttrin-requery.el
index 5d52b36..e065c43 100644
--- a/tests/test-wttrin-requery.el
+++ b/tests/test-wttrin-requery.el
@@ -39,7 +39,7 @@
(test-wttrin-requery-setup)
(unwind-protect
(let ((old-buffer (get-buffer-create "*wttr.in*")))
- (cl-letf (((symbol-function 'wttrin-query) (lambda (_loc) nil)))
+ (cl-letf (((symbol-function 'wttrin-query) (lambda (_loc &rest _) nil)))
(wttrin--requery-location "Tokyo")
;; Old buffer should be dead
(should-not (buffer-live-p old-buffer))))
@@ -51,7 +51,7 @@
(unwind-protect
(let ((queried-location nil))
(cl-letf (((symbol-function 'wttrin-query)
- (lambda (loc) (setq queried-location loc))))
+ (lambda (loc &rest _) (setq queried-location loc))))
(wttrin--requery-location "Berlin, DE")
(should (equal queried-location "Berlin, DE"))))
(test-wttrin-requery-teardown)))
@@ -66,7 +66,7 @@
;; Ensure no buffer exists
(should-not (get-buffer "*wttr.in*"))
(cl-letf (((symbol-function 'wttrin-query)
- (lambda (loc) (setq queried-location loc))))
+ (lambda (loc &rest _) (setq queried-location loc))))
(wttrin--requery-location "Paris")
(should (equal queried-location "Paris"))))
(test-wttrin-requery-teardown)))
@@ -77,7 +77,7 @@
(unwind-protect
(let ((queried-location nil))
(cl-letf (((symbol-function 'wttrin-query)
- (lambda (loc) (setq queried-location loc))))
+ (lambda (loc &rest _) (setq queried-location loc))))
(wttrin--requery-location "Zürich, CH")
(should (equal queried-location "Zürich, CH"))))
(test-wttrin-requery-teardown)))
@@ -113,7 +113,7 @@ to the core requery function."
(setq offered-collection collection)
"Paris"))
((symbol-function 'wttrin--requery-location)
- (lambda (_loc) nil)))
+ (lambda (_loc &rest _) nil)))
(wttrin-requery)
;; The collection is now a completion table (a function) that pins
;; the sentinel first; check the candidates it completes over.
@@ -134,7 +134,7 @@ to the core requery function."
(setq initial-input init)
"Solo City"))
((symbol-function 'wttrin--requery-location)
- (lambda (_loc) nil)))
+ (lambda (_loc &rest _) nil)))
(wttrin-requery)
(should (equal initial-input "Solo City"))))
(test-wttrin-requery-teardown)))
@@ -150,7 +150,7 @@ to the core requery function."
(setq initial-input init)
"Paris"))
((symbol-function 'wttrin--requery-location)
- (lambda (_loc) nil)))
+ (lambda (_loc &rest _) nil)))
(wttrin-requery)
(should-not initial-input)))
(test-wttrin-requery-teardown)))
diff --git a/tests/test-wttrin-saved-locations.el b/tests/test-wttrin-saved-locations.el
new file mode 100644
index 0000000..0a7b8b3
--- /dev/null
+++ b/tests/test-wttrin-saved-locations.el
@@ -0,0 +1,360 @@
+;;; test-wttrin-saved-locations.el --- Tests for the named-locations directory -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2024-2026 Craig Jennings
+
+;;; Commentary:
+;; Unit tests for the named-locations directory (Phase 1): the normalizer
+;; `wttrin--saved-locations', the resolver `wttrin--resolve-location-query',
+;; candidate de-duplication/precedence, favorite-as-name resolution, alias cache
+;; identity, history suppression of saved names, and savehist registration.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;;; wttrin--saved-locations (normalizer)
+
+(ert-deftest test-wttrin-saved-locations-normal-pairs-returned ()
+ "Normal: well-formed pairs are returned as (NAME . QUERY)."
+ (let ((wttrin-saved-locations '(("Home" . "1500 Sugar Bowl Dr, New Orleans"))))
+ (should (equal '(("Home" . "1500 Sugar Bowl Dr, New Orleans"))
+ (wttrin--saved-locations)))))
+
+(ert-deftest test-wttrin-saved-locations-boundary-bare-string-shorthand ()
+ "Boundary: a bare string S becomes (S . S)."
+ (let ((wttrin-saved-locations '("Berkeley, CA")))
+ (should (equal '(("Berkeley, CA" . "Berkeley, CA"))
+ (wttrin--saved-locations)))))
+
+(ert-deftest test-wttrin-saved-locations-boundary-whitespace-trimmed ()
+ "Boundary: surrounding whitespace on name and query is trimmed."
+ (let ((wttrin-saved-locations '((" Home " . " Paris, FR "))))
+ (should (equal '(("Home" . "Paris, FR")) (wttrin--saved-locations)))))
+
+(ert-deftest test-wttrin-saved-locations-error-malformed-skipped ()
+ "Error: non-cons, non-string, and empty entries are skipped, not fatal."
+ (let ((wttrin-saved-locations
+ (list '("Good" . "Tokyo") 42 '("" . "x") '("y" . "") " " '(a . b))))
+ (should (equal '(("Good" . "Tokyo")) (wttrin--saved-locations)))))
+
+;;; wttrin--resolve-location-query
+
+(ert-deftest test-wttrin-saved-locations-normal-resolve-name-to-query ()
+ "Normal: a saved name resolves to its query."
+ (let ((wttrin-saved-locations '(("Craig's House" . "1500 Sugar Bowl Dr, New Orleans"))))
+ (should (equal "1500 Sugar Bowl Dr, New Orleans"
+ (wttrin--resolve-location-query "Craig's House")))))
+
+(ert-deftest test-wttrin-saved-locations-boundary-resolve-passthrough ()
+ "Boundary: a non-saved selection passes through unchanged."
+ (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))))
+ (should (equal "Tokyo, JP" (wttrin--resolve-location-query "Tokyo, JP")))))
+
+;;; Candidate de-duplication and precedence
+
+(ert-deftest test-wttrin-saved-locations-normal-candidates-precedence ()
+ "Normal: candidates are saved, favorite, defaults, then history, deduped."
+ (testutil-wttrin-setup)
+ (unwind-protect
+ (let ((wttrin-geolocation-enabled nil)
+ (wttrin-saved-locations '(("Home" . "Paris, FR")))
+ (wttrin-favorite-location "Reykjavik")
+ (wttrin-default-locations '("Honolulu, HI"))
+ (wttrin--location-history '("Tokyo")))
+ (should (equal '("Home" "Reykjavik" "Honolulu, HI" "Tokyo")
+ (wttrin--completion-candidates))))
+ (testutil-wttrin-teardown)))
+
+(ert-deftest test-wttrin-saved-locations-boundary-candidates-dedup-saved-wins ()
+ "Boundary: a name present in saved and defaults appears once (saved first)."
+ (testutil-wttrin-setup)
+ (unwind-protect
+ (let ((wttrin-geolocation-enabled nil)
+ (wttrin-saved-locations '(("Honolulu, HI" . "Honolulu, HI")))
+ (wttrin-favorite-location nil)
+ (wttrin-default-locations '("Honolulu, HI" "Berkeley, CA"))
+ (wttrin--location-history nil))
+ (should (equal '("Honolulu, HI" "Berkeley, CA")
+ (wttrin--completion-candidates))))
+ (testutil-wttrin-teardown)))
+
+;;; Favorite-as-name resolution
+
+(ert-deftest test-wttrin-saved-locations-normal-favorite-name-resolves-to-query ()
+ "Normal: a favorite that is a saved name resolves to its query for fetching."
+ (let ((wttrin-saved-locations '(("Home" . "1500 Sugar Bowl Dr, New Orleans")))
+ (wttrin-favorite-location "Home"))
+ (should (equal "1500 Sugar Bowl Dr, New Orleans"
+ (wttrin--resolve-favorite-location)))))
+
+(ert-deftest test-wttrin-saved-locations-normal-favorite-display-shows-name ()
+ "Normal: the favorite display name is the saved name, not its query."
+ (let ((wttrin-saved-locations '(("Home" . "1500 Sugar Bowl Dr, New Orleans")))
+ (wttrin-favorite-location "Home"))
+ (should (equal "Home" (wttrin--favorite-location-display-name)))))
+
+;;; Alias cache identity
+
+(ert-deftest test-wttrin-saved-locations-normal-cache-keyed-on-query ()
+ "Normal: cache identity follows the query, not the display name.
+Two names with the same query share a key; the name never leaks into the key."
+ (let ((wttrin-saved-locations '(("A" . "Paris, FR") ("B" . "Paris, FR"))))
+ (should (equal (wttrin--make-cache-key (wttrin--resolve-location-query "A"))
+ (wttrin--make-cache-key (wttrin--resolve-location-query "B"))))))
+
+;;; History suppression of saved names
+
+(ert-deftest test-wttrin-saved-locations-boundary-saved-name-not-logged ()
+ "Boundary: a saved-directory name is not added to history."
+ (testutil-wttrin-setup)
+ (unwind-protect
+ (let ((wttrin-saved-locations '(("Home" . "Paris, FR")))
+ (wttrin-default-locations '())
+ (wttrin--location-history nil))
+ (wttrin--add-to-location-history "Home")
+ (should (null wttrin--location-history)))
+ (testutil-wttrin-teardown)))
+
+;;; savehist
+
+(ert-deftest test-wttrin-saved-locations-integration-savehist-registers ()
+ "Integration: wttrin-saved-locations is registered for savehist persistence."
+ (require 'savehist)
+ (let ((savehist-additional-variables '(kill-ring)))
+ (wttrin--savehist-register)
+ (should (memq 'wttrin-saved-locations savehist-additional-variables))))
+
+;;; wttrin--coordinates-p
+
+(ert-deftest test-wttrin-saved-locations-normal-coordinates-p ()
+ "Normal/Boundary: coordinate strings match; place names do not."
+ (should (wttrin--coordinates-p "41.37,-71.83"))
+ (should (wttrin--coordinates-p "1.5,2.5"))
+ (should-not (wttrin--coordinates-p "New York, NY"))
+ (should-not (wttrin--coordinates-p "Berkeley, CA"))
+ (should-not (wttrin--coordinates-p "")))
+
+;;; wttrin--put-saved-location
+
+(ert-deftest test-wttrin-saved-locations-normal-put-adds-and-updates ()
+ "Normal: put adds a new entry and updates an existing name without duplicating."
+ (let ((wttrin-saved-locations nil))
+ (wttrin--put-saved-location "Home" "Paris, FR")
+ (should (equal "Paris, FR" (wttrin--resolve-location-query "Home")))
+ (wttrin--put-saved-location "Home" "Tokyo, JP")
+ (should (equal "Tokyo, JP" (wttrin--resolve-location-query "Home")))
+ (should (= 1 (length (wttrin--saved-locations))))))
+
+(ert-deftest test-wttrin-saved-locations-error-put-rejects-empty-and-sentinel ()
+ "Error: put refuses an empty name, empty query, or the sentinel name."
+ (let ((wttrin-saved-locations nil))
+ (should-error (wttrin--put-saved-location "" "Paris") :type 'user-error)
+ (should-error (wttrin--put-saved-location "Home" "") :type 'user-error)
+ (should-error (wttrin--put-saved-location wttrin--geolocation-sentinel "x")
+ :type 'user-error)))
+
+;;; wttrin-rename-location
+
+(ert-deftest test-wttrin-saved-locations-normal-rename ()
+ "Normal: rename moves the entry and updates the favorite reference."
+ (let ((wttrin-saved-locations '(("Home" . "Paris, FR")))
+ (wttrin-favorite-location "Home"))
+ (cl-letf (((symbol-function 'message) (lambda (&rest _) nil)))
+ (wttrin-rename-location "Home" "Casa"))
+ (should (equal "Paris, FR" (wttrin--resolve-location-query "Casa")))
+ (should-not (assoc "Home" (wttrin--saved-locations)))
+ (should (equal "Casa" wttrin-favorite-location))))
+
+(ert-deftest test-wttrin-saved-locations-error-rename-collision-refused ()
+ "Error: renaming onto an existing name is refused and changes nothing."
+ (let ((wttrin-saved-locations '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP"))))
+ (should-error (wttrin-rename-location "Home" "Work") :type 'user-error)
+ (should (equal "Paris, FR" (wttrin--resolve-location-query "Home")))))
+
+(ert-deftest test-wttrin-saved-locations-normal-rename-favorite-refreshes-mode-line ()
+ "Normal: renaming the favorite refreshes the mode-line so the icon and tooltip
+follow the new name immediately instead of at the next scheduled fetch."
+ (let ((wttrin-saved-locations '(("Home" . "Paris, FR")))
+ (wttrin-favorite-location "Home")
+ (wttrin--location-history nil)
+ (wttrin-mode-line-mode t)
+ (fetched nil))
+ (cl-letf (((symbol-function 'wttrin--mode-line-fetch-weather)
+ (lambda () (setq fetched t)))
+ ((symbol-function 'wttrin--mode-line-set-placeholder)
+ (lambda () nil))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (wttrin-rename-location "Home" "Casa"))
+ (should (equal "Casa" wttrin-favorite-location))
+ (should fetched)))
+
+(ert-deftest test-wttrin-saved-locations-boundary-rename-non-favorite-no-refresh ()
+ "Boundary: renaming a location that is not the favorite does not refresh."
+ (let ((wttrin-saved-locations '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP")))
+ (wttrin-favorite-location "Work")
+ (wttrin--location-history nil)
+ (wttrin-mode-line-mode t)
+ (fetched nil))
+ (cl-letf (((symbol-function 'wttrin--mode-line-fetch-weather)
+ (lambda () (setq fetched t)))
+ ((symbol-function 'wttrin--mode-line-set-placeholder)
+ (lambda () nil))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (wttrin-rename-location "Home" "Casa"))
+ (should-not fetched)))
+
+;;; wttrin-remove-location
+
+(ert-deftest test-wttrin-saved-locations-normal-remove-confirmed ()
+ "Normal: confirming removes the entry."
+ (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))))
+ (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (wttrin-remove-location "Home"))
+ (should (null (wttrin--saved-locations)))))
+
+(ert-deftest test-wttrin-saved-locations-boundary-remove-declined-keeps ()
+ "Boundary: declining the confirmation keeps the entry."
+ (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))))
+ (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) nil))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (wttrin-remove-location "Home"))
+ (should (assoc "Home" (wttrin--saved-locations)))))
+
+(ert-deftest test-wttrin-saved-locations-normal-remove-favorite-refreshes-mode-line ()
+ "Normal: removing the favorite refreshes the mode-line so it stops showing the
+now-deleted alias's resolved weather and re-fetches against the bare query."
+ (let ((wttrin-saved-locations '(("Home" . "Paris, FR")))
+ (wttrin-favorite-location "Home")
+ (wttrin-mode-line-mode t)
+ (fetched nil))
+ (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t))
+ ((symbol-function 'wttrin--mode-line-fetch-weather)
+ (lambda () (setq fetched t)))
+ ((symbol-function 'wttrin--mode-line-set-placeholder)
+ (lambda () nil))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (wttrin-remove-location "Home"))
+ (should fetched)))
+
+(ert-deftest test-wttrin-saved-locations-boundary-remove-non-favorite-no-refresh ()
+ "Boundary: removing a location that is not the favorite does not refresh."
+ (let ((wttrin-saved-locations '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP")))
+ (wttrin-favorite-location "Work")
+ (wttrin-mode-line-mode t)
+ (fetched nil))
+ (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t))
+ ((symbol-function 'wttrin--mode-line-fetch-weather)
+ (lambda () (setq fetched t)))
+ ((symbol-function 'wttrin--mode-line-set-placeholder)
+ (lambda () nil))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (wttrin-remove-location "Home"))
+ (should-not fetched)))
+
+;;; wttrin-make-default — geolocation naming
+
+(ert-deftest test-wttrin-saved-locations-normal-d-names-and-promotes ()
+ "Normal: d on a coordinate buffer names it, saves it, and promotes the name."
+ (let ((wttrin-saved-locations nil)
+ (wttrin-favorite-location nil)
+ (wttrin-mode-line-mode nil))
+ (with-temp-buffer
+ (setq-local wttrin--current-location "41.37,-71.83")
+ (setq-local wttrin--current-display "41.37,-71.83")
+ (setq-local wttrin--current-address "Westerly, RI")
+ (cl-letf (((symbol-function 'read-string) (lambda (&rest _) "Home"))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (wttrin-make-default)))
+ (should (equal "41.37,-71.83" (wttrin--resolve-location-query "Home")))
+ (should (equal "Home" wttrin-favorite-location))))
+
+(ert-deftest test-wttrin-saved-locations-boundary-d-empty-keeps-coordinates ()
+ "Boundary: an empty name at the d prompt keeps the coordinates, saves no entry."
+ (let ((wttrin-saved-locations nil)
+ (wttrin-favorite-location nil)
+ (wttrin-mode-line-mode nil))
+ (with-temp-buffer
+ (setq-local wttrin--current-location "41.37,-71.83")
+ (setq-local wttrin--current-address "Westerly, RI")
+ (cl-letf (((symbol-function 'read-string) (lambda (&rest _) ""))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (wttrin-make-default)))
+ (should (null (wttrin--saved-locations)))
+ (should (equal "41.37,-71.83" wttrin-favorite-location))))
+
+(ert-deftest test-wttrin-saved-locations-boundary-d-named-buffer-no-prompt ()
+ "Boundary: d on a named buffer promotes the display name without prompting."
+ (let ((wttrin-saved-locations nil)
+ (wttrin-favorite-location nil)
+ (wttrin-mode-line-mode nil)
+ (prompted nil))
+ (with-temp-buffer
+ (setq-local wttrin--current-location "1500 Sugar Bowl Dr")
+ (setq-local wttrin--current-display "Craig's House")
+ (cl-letf (((symbol-function 'read-string)
+ (lambda (&rest _) (setq prompted t) "x"))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (wttrin-make-default)))
+ (should-not prompted)
+ (should (equal "Craig's House" wttrin-favorite-location))))
+
+;;; interactive entry smoke tests (cover the prompt forms)
+
+(ert-deftest test-wttrin-saved-locations-normal-save-location-interactive ()
+ "Normal: the interactive save command reads the buffer query and a name."
+ (let ((wttrin-saved-locations nil))
+ (with-temp-buffer
+ (setq-local wttrin--current-location "Paris, FR")
+ (setq-local wttrin--current-display "Paris, FR")
+ (cl-letf (((symbol-function 'read-string) (lambda (&rest _) "Home"))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (call-interactively 'wttrin-save-location)))
+ (should (equal "Paris, FR" (wttrin--resolve-location-query "Home")))))
+
+(ert-deftest test-wttrin-saved-locations-normal-rename-interactive ()
+ "Normal: the interactive rename command prompts for the entry and new name."
+ (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))))
+ (cl-letf (((symbol-function 'completing-read) (lambda (&rest _) "Home"))
+ ((symbol-function 'read-string) (lambda (&rest _) "Casa"))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (call-interactively 'wttrin-rename-location))
+ (should (equal "Paris, FR" (wttrin--resolve-location-query "Casa")))))
+
+(ert-deftest test-wttrin-saved-locations-normal-remove-interactive ()
+ "Normal: the interactive remove command prompts and confirms."
+ (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))))
+ (cl-letf (((symbol-function 'completing-read) (lambda (&rest _) "Home"))
+ ((symbol-function 'yes-or-no-p) (lambda (&rest _) t))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (call-interactively 'wttrin-remove-location))
+ (should (null (wttrin--saved-locations)))))
+
+(ert-deftest test-wttrin-saved-locations-boundary-save-empty-name-cancels ()
+ "Boundary: an empty name at the save prompt cancels without saving."
+ (let ((wttrin-saved-locations nil))
+ (with-temp-buffer
+ (setq-local wttrin--current-location "Paris, FR")
+ (cl-letf (((symbol-function 'read-string) (lambda (&rest _) " "))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (call-interactively 'wttrin-save-location)))
+ (should (null (wttrin--saved-locations)))))
+
+;;; coordinate suppression in history
+
+(ert-deftest test-wttrin-saved-locations-boundary-coordinates-not-logged ()
+ "Boundary: a raw coordinate string is never added to history."
+ (testutil-wttrin-setup)
+ (unwind-protect
+ (let ((wttrin-default-locations '())
+ (wttrin-saved-locations nil)
+ (wttrin--location-history nil))
+ (wttrin--add-to-location-history "41.37,-71.83")
+ (should (null wttrin--location-history)))
+ (testutil-wttrin-teardown)))
+
+(provide 'test-wttrin-saved-locations)
+;;; test-wttrin-saved-locations.el ends here
diff --git a/tests/test-wttrin-set-location-from-geolocation.el b/tests/test-wttrin-set-location-from-geolocation.el
index e7c3a97..f16b2ae 100644
--- a/tests/test-wttrin-set-location-from-geolocation.el
+++ b/tests/test-wttrin-set-location-from-geolocation.el
@@ -65,6 +65,29 @@
(should (string= "Pre-existing, Place" wttrin-favorite-location)))
(test-wttrin-set-location-from-geolocation-teardown)))
+(ert-deftest test-wttrin-set-location-from-geolocation-normal-confirm-refreshes-mode-line ()
+ "Normal: a confirmed detection refreshes the mode-line so it tracks the new
+favorite immediately instead of at the next scheduled fetch."
+ (test-wttrin-set-location-from-geolocation-setup)
+ (setq wttrin-favorite-location "Pre-existing, Place")
+ (unwind-protect
+ (let ((wttrin-geolocation-enabled t)
+ (wttrin-mode-line-mode t)
+ (wttrin--location-history nil)
+ (fetched nil))
+ (cl-letf (((symbol-function 'wttrin-geolocation-detect)
+ (lambda (callback) (funcall callback "Berkeley, California")))
+ ((symbol-function 'yes-or-no-p) (lambda (&rest _) t))
+ ((symbol-function 'message) (lambda (&rest _) nil))
+ ((symbol-function 'wttrin--mode-line-fetch-weather)
+ (lambda () (setq fetched t)))
+ ((symbol-function 'wttrin--mode-line-set-placeholder)
+ (lambda () nil)))
+ (wttrin-set-location-from-geolocation))
+ (should (string= "Berkeley, California" wttrin-favorite-location))
+ (should fetched))
+ (test-wttrin-set-location-from-geolocation-teardown)))
+
;;; Boundary Cases
(ert-deftest test-wttrin-set-location-from-geolocation-boundary-unicode-location ()
diff --git a/tests/test-wttrin-use-current-location.el b/tests/test-wttrin-use-current-location.el
index 2b08448..4b61657 100644
--- a/tests/test-wttrin-use-current-location.el
+++ b/tests/test-wttrin-use-current-location.el
@@ -52,6 +52,27 @@
(should (equal "Berkeley, CA" wttrin-favorite-location)))
(test-wttrin-use-current-location-teardown)))
+(ert-deftest test-wttrin-use-current-location-normal-confirm-refreshes-mode-line ()
+ "Normal: confirming refreshes the mode-line so it switches to the current
+location immediately rather than at the next scheduled fetch."
+ (test-wttrin-use-current-location-setup)
+ (setq wttrin-favorite-location "Berkeley, CA")
+ (unwind-protect
+ (let ((wttrin-geolocation-enabled t)
+ (wttrin-mode-line-mode t)
+ (wttrin--location-history nil)
+ (fetched nil))
+ (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t))
+ ((symbol-function 'message) (lambda (&rest _) nil))
+ ((symbol-function 'wttrin--mode-line-fetch-weather)
+ (lambda () (setq fetched t)))
+ ((symbol-function 'wttrin--mode-line-set-placeholder)
+ (lambda () nil)))
+ (wttrin-use-current-location))
+ (should (eq t wttrin-favorite-location))
+ (should fetched))
+ (test-wttrin-use-current-location-teardown)))
+
;;; Boundary / Error Cases
(ert-deftest test-wttrin-use-current-location-boundary-disabled-no-prompt-no-set ()