summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test-wttrin--buffer-cache-refresh.el120
-rw-r--r--tests/test-wttrin--mode-line-start.el157
-rw-r--r--tests/test-wttrin--mode-line-stop.el104
-rw-r--r--tests/test-wttrin--save-debug-data.el144
-rw-r--r--tests/test-wttrin-clear-cache.el69
-rw-r--r--tests/test-wttrin-fetch-raw-string.el65
-rw-r--r--tests/test-wttrin-mode-line-click.el69
-rw-r--r--tests/test-wttrin-mode-line-force-refresh.el71
-rw-r--r--tests/test-wttrin-query.el122
-rw-r--r--tests/test-wttrin-requery-force.el91
-rw-r--r--tests/test-wttrin-requery.el152
11 files changed, 1164 insertions, 0 deletions
diff --git a/tests/test-wttrin--buffer-cache-refresh.el b/tests/test-wttrin--buffer-cache-refresh.el
new file mode 100644
index 0000000..a407450
--- /dev/null
+++ b/tests/test-wttrin--buffer-cache-refresh.el
@@ -0,0 +1,120 @@
+;;; test-wttrin--buffer-cache-refresh.el --- Tests for wttrin--buffer-cache-refresh -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for wttrin--buffer-cache-refresh function.
+;; Tests the proactive background refresh that keeps buffer cache fresh.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;;; Setup and Teardown
+
+(defun test-wttrin--buffer-cache-refresh-setup ()
+ "Setup for buffer-cache-refresh tests."
+ (testutil-wttrin-setup))
+
+(defun test-wttrin--buffer-cache-refresh-teardown ()
+ "Teardown for buffer-cache-refresh tests."
+ (testutil-wttrin-teardown))
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin--buffer-cache-refresh-normal-success-updates-cache ()
+ "Successful fetch should store fresh data in the buffer cache."
+ (test-wttrin--buffer-cache-refresh-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris"))
+ (testutil-wttrin-mock-http-response "Fresh weather data for Paris"
+ (wttrin--buffer-cache-refresh)
+ ;; Cache should now have an entry for Paris
+ (let* ((cache-key (wttrin--make-cache-key "Paris"))
+ (cached (gethash cache-key wttrin--cache)))
+ (should cached)
+ (should (equal (cdr cached) "Fresh weather data for Paris")))))
+ (test-wttrin--buffer-cache-refresh-teardown)))
+
+(ert-deftest test-wttrin--buffer-cache-refresh-normal-uses-favorite-location ()
+ "Refresh should fetch weather for the configured favorite location."
+ (test-wttrin--buffer-cache-refresh-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Tokyo, JP")
+ (fetched-query nil))
+ (cl-letf (((symbol-function 'wttrin-fetch-raw-string)
+ (lambda (query callback)
+ (setq fetched-query query)
+ (funcall callback "some data"))))
+ (wttrin--buffer-cache-refresh)
+ (should (equal fetched-query "Tokyo, JP"))))
+ (test-wttrin--buffer-cache-refresh-teardown)))
+
+(ert-deftest test-wttrin--buffer-cache-refresh-normal-cache-key-respects-unit-system ()
+ "Cache entry should use the correct key based on unit system settings."
+ (test-wttrin--buffer-cache-refresh-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris")
+ (wttrin-unit-system "m"))
+ (testutil-wttrin-mock-http-response "metric data"
+ (wttrin--buffer-cache-refresh)
+ ;; Cache key should include unit system
+ (let* ((expected-key (wttrin--make-cache-key "Paris"))
+ (cached (gethash expected-key wttrin--cache)))
+ (should cached)
+ (should (equal (cdr cached) "metric data")))))
+ (test-wttrin--buffer-cache-refresh-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin--buffer-cache-refresh-boundary-nil-location-is-noop ()
+ "When favorite-location is nil, no fetch should be attempted."
+ (test-wttrin--buffer-cache-refresh-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location nil)
+ (fetch-called nil))
+ (cl-letf (((symbol-function 'wttrin-fetch-raw-string)
+ (lambda (_query _callback) (setq fetch-called t))))
+ (wttrin--buffer-cache-refresh)
+ (should-not fetch-called)
+ (should (= 0 (testutil-wttrin-cache-size)))))
+ (test-wttrin--buffer-cache-refresh-teardown)))
+
+(ert-deftest test-wttrin--buffer-cache-refresh-boundary-overwrites-stale-entry ()
+ "A refresh should replace any existing stale cache entry for the same location."
+ (test-wttrin--buffer-cache-refresh-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris"))
+ ;; Seed cache with old data
+ (testutil-wttrin-add-to-cache "Paris" "old stale data" 9999)
+ (testutil-wttrin-mock-http-response "fresh new data"
+ (wttrin--buffer-cache-refresh)
+ (let* ((cache-key (wttrin--make-cache-key "Paris"))
+ (cached (gethash cache-key wttrin--cache)))
+ (should (equal (cdr cached) "fresh new data")))))
+ (test-wttrin--buffer-cache-refresh-teardown)))
+
+;;; Error Cases
+
+(ert-deftest test-wttrin--buffer-cache-refresh-error-fetch-failure-preserves-cache ()
+ "Failed fetch should not overwrite existing cache entry."
+ (test-wttrin--buffer-cache-refresh-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris"))
+ ;; Seed cache with existing data
+ (testutil-wttrin-add-to-cache "Paris" "existing good data" 300)
+ (cl-letf (((symbol-function 'wttrin-fetch-raw-string)
+ (lambda (_query callback) (funcall callback nil))))
+ (wttrin--buffer-cache-refresh)
+ ;; Existing cache should be untouched
+ (let* ((cache-key (wttrin--make-cache-key "Paris"))
+ (cached (gethash cache-key wttrin--cache)))
+ (should cached)
+ (should (equal (cdr cached) "existing good data")))))
+ (test-wttrin--buffer-cache-refresh-teardown)))
+
+(provide 'test-wttrin--buffer-cache-refresh)
+;;; test-wttrin--buffer-cache-refresh.el ends here
diff --git a/tests/test-wttrin--mode-line-start.el b/tests/test-wttrin--mode-line-start.el
new file mode 100644
index 0000000..e97c185
--- /dev/null
+++ b/tests/test-wttrin--mode-line-start.el
@@ -0,0 +1,157 @@
+;;; test-wttrin--mode-line-start.el --- Tests for wttrin--mode-line-start -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for wttrin--mode-line-start function.
+;; Tests that starting mode-line display sets up placeholder, timers, and scheduling.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;;; Setup and Teardown
+
+(defun test-wttrin--mode-line-start-setup ()
+ "Setup for mode-line-start tests."
+ (testutil-wttrin-setup)
+ (setq wttrin-mode-line-string nil)
+ (setq wttrin--mode-line-cache nil)
+ (setq wttrin--mode-line-timer nil)
+ (setq wttrin--buffer-refresh-timer nil))
+
+(defun test-wttrin--mode-line-start-teardown ()
+ "Teardown for mode-line-start tests."
+ (testutil-wttrin-teardown)
+ (when (timerp wttrin--mode-line-timer)
+ (cancel-timer wttrin--mode-line-timer))
+ (when (timerp wttrin--buffer-refresh-timer)
+ (cancel-timer wttrin--buffer-refresh-timer))
+ (setq wttrin-mode-line-string nil)
+ (setq wttrin--mode-line-cache nil)
+ (setq wttrin--mode-line-timer nil)
+ (setq wttrin--buffer-refresh-timer nil))
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin--mode-line-start-normal-shows-placeholder-immediately ()
+ "Starting mode-line should show the hourglass placeholder right away."
+ (test-wttrin--mode-line-start-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris")
+ (wttrin-mode-line-emoji-font nil))
+ ;; Mock run-at-time so no real timers fire
+ (cl-letf (((symbol-function 'run-at-time)
+ (lambda (_time _repeat _func) (list 'mock-timer))))
+ (wttrin--mode-line-start)
+ ;; Placeholder should be visible
+ (should wttrin-mode-line-string)
+ (should (string-match-p "⏳" (substring-no-properties wttrin-mode-line-string)))))
+ (test-wttrin--mode-line-start-teardown)))
+
+(ert-deftest test-wttrin--mode-line-start-normal-schedules-delayed-initial-fetch ()
+ "Initial weather fetch should be scheduled after startup-delay seconds."
+ (test-wttrin--mode-line-start-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris")
+ (wttrin-mode-line-startup-delay 5)
+ (scheduled-calls nil))
+ (cl-letf (((symbol-function 'run-at-time)
+ (lambda (time repeat func)
+ (push (list :time time :repeat repeat :func func) scheduled-calls)
+ (list 'mock-timer))))
+ (wttrin--mode-line-start)
+ ;; One of the calls should be the delayed initial fetch
+ (let ((initial-fetch (seq-find (lambda (call)
+ (and (= (plist-get call :time) 5)
+ (null (plist-get call :repeat))))
+ scheduled-calls)))
+ (should initial-fetch)
+ (should (eq (plist-get initial-fetch :func)
+ #'wttrin--mode-line-fetch-weather)))))
+ (test-wttrin--mode-line-start-teardown)))
+
+(ert-deftest test-wttrin--mode-line-start-normal-creates-repeating-mode-line-timer ()
+ "A repeating timer should be created for periodic mode-line refresh."
+ (test-wttrin--mode-line-start-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris")
+ (wttrin-mode-line-refresh-interval 3600)
+ (scheduled-calls nil))
+ (cl-letf (((symbol-function 'run-at-time)
+ (lambda (time repeat func)
+ (push (list :time time :repeat repeat :func func) scheduled-calls)
+ (list 'mock-timer))))
+ (wttrin--mode-line-start)
+ ;; Should have a repeating timer for mode-line fetch
+ (let ((repeating-fetch (seq-find (lambda (call)
+ (and (equal (plist-get call :repeat) 3600)
+ (eq (plist-get call :func)
+ #'wttrin--mode-line-fetch-weather)))
+ scheduled-calls)))
+ (should repeating-fetch))))
+ (test-wttrin--mode-line-start-teardown)))
+
+(ert-deftest test-wttrin--mode-line-start-normal-creates-buffer-refresh-timer ()
+ "A repeating timer should be created for periodic buffer cache refresh."
+ (test-wttrin--mode-line-start-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris")
+ (wttrin-refresh-interval 3600)
+ (scheduled-calls nil))
+ (cl-letf (((symbol-function 'run-at-time)
+ (lambda (time repeat func)
+ (push (list :time time :repeat repeat :func func) scheduled-calls)
+ (list 'mock-timer))))
+ (wttrin--mode-line-start)
+ ;; Should have a repeating timer for buffer cache refresh
+ (let ((buffer-refresh (seq-find (lambda (call)
+ (and (equal (plist-get call :repeat) 3600)
+ (eq (plist-get call :func)
+ #'wttrin--buffer-cache-refresh)))
+ scheduled-calls)))
+ (should buffer-refresh))))
+ (test-wttrin--mode-line-start-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin--mode-line-start-boundary-nil-location-skips-setup ()
+ "When no favorite location is set, no timers or placeholder should be created."
+ (test-wttrin--mode-line-start-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location nil)
+ (run-at-time-called nil))
+ (cl-letf (((symbol-function 'run-at-time)
+ (lambda (_time _repeat _func)
+ (setq run-at-time-called t)
+ (list 'mock-timer))))
+ (wttrin--mode-line-start)
+ (should-not run-at-time-called)
+ (should-not wttrin-mode-line-string)
+ (should-not wttrin--mode-line-timer)))
+ (test-wttrin--mode-line-start-teardown)))
+
+(ert-deftest test-wttrin--mode-line-start-boundary-cancels-existing-timers ()
+ "Starting when timers already exist should cancel old timers before creating new ones."
+ (test-wttrin--mode-line-start-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris")
+ (cancelled-timers nil))
+ ;; Create fake existing timers
+ (setq wttrin--mode-line-timer (run-at-time 99999 nil #'ignore))
+ (setq wttrin--buffer-refresh-timer (run-at-time 99999 nil #'ignore))
+ (let ((old-ml-timer wttrin--mode-line-timer)
+ (old-buf-timer wttrin--buffer-refresh-timer))
+ (cl-letf (((symbol-function 'run-at-time)
+ (lambda (_time _repeat _func) (list 'mock-timer))))
+ (wttrin--mode-line-start)
+ ;; Old timers should have been replaced
+ (should-not (eq wttrin--mode-line-timer old-ml-timer))
+ (should-not (eq wttrin--buffer-refresh-timer old-buf-timer)))))
+ (test-wttrin--mode-line-start-teardown)))
+
+(provide 'test-wttrin--mode-line-start)
+;;; test-wttrin--mode-line-start.el ends here
diff --git a/tests/test-wttrin--mode-line-stop.el b/tests/test-wttrin--mode-line-stop.el
new file mode 100644
index 0000000..e791ee5
--- /dev/null
+++ b/tests/test-wttrin--mode-line-stop.el
@@ -0,0 +1,104 @@
+;;; test-wttrin--mode-line-stop.el --- Tests for wttrin--mode-line-stop -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for wttrin--mode-line-stop function.
+;; Tests that stopping mode-line display cleans up all state properly.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;;; Setup and Teardown
+
+(defun test-wttrin--mode-line-stop-setup ()
+ "Setup for mode-line-stop tests."
+ (testutil-wttrin-setup)
+ (setq wttrin-mode-line-string nil)
+ (setq wttrin--mode-line-cache nil)
+ (setq wttrin--mode-line-timer nil)
+ (setq wttrin--buffer-refresh-timer nil))
+
+(defun test-wttrin--mode-line-stop-teardown ()
+ "Teardown for mode-line-stop tests."
+ (testutil-wttrin-teardown)
+ ;; Cancel any real timers that may have been created
+ (when (timerp wttrin--mode-line-timer)
+ (cancel-timer wttrin--mode-line-timer))
+ (when (timerp wttrin--buffer-refresh-timer)
+ (cancel-timer wttrin--buffer-refresh-timer))
+ (setq wttrin-mode-line-string nil)
+ (setq wttrin--mode-line-cache nil)
+ (setq wttrin--mode-line-timer nil)
+ (setq wttrin--buffer-refresh-timer nil))
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin--mode-line-stop-normal-clears-mode-line-string ()
+ "After stop, mode-line-string should be nil so nothing shows in the mode-line."
+ (test-wttrin--mode-line-stop-setup)
+ (unwind-protect
+ (progn
+ (setq wttrin-mode-line-string "some weather display")
+ (wttrin--mode-line-stop)
+ (should-not wttrin-mode-line-string))
+ (test-wttrin--mode-line-stop-teardown)))
+
+(ert-deftest test-wttrin--mode-line-stop-normal-clears-mode-line-cache ()
+ "After stop, cached mode-line data should be discarded."
+ (test-wttrin--mode-line-stop-setup)
+ (unwind-protect
+ (progn
+ (setq wttrin--mode-line-cache (cons (float-time) "Paris: ☀️ +61°F Clear"))
+ (wttrin--mode-line-stop)
+ (should-not wttrin--mode-line-cache))
+ (test-wttrin--mode-line-stop-teardown)))
+
+(ert-deftest test-wttrin--mode-line-stop-normal-cancels-mode-line-timer ()
+ "After stop, the mode-line refresh timer should be cancelled and nil."
+ (test-wttrin--mode-line-stop-setup)
+ (unwind-protect
+ (progn
+ ;; Create a real timer so cancel-timer has something to work with
+ (setq wttrin--mode-line-timer
+ (run-at-time 99999 nil #'ignore))
+ (wttrin--mode-line-stop)
+ (should-not wttrin--mode-line-timer))
+ (test-wttrin--mode-line-stop-teardown)))
+
+(ert-deftest test-wttrin--mode-line-stop-normal-cancels-buffer-refresh-timer ()
+ "After stop, the buffer-refresh timer should be cancelled and nil."
+ (test-wttrin--mode-line-stop-setup)
+ (unwind-protect
+ (progn
+ (setq wttrin--buffer-refresh-timer
+ (run-at-time 99999 nil #'ignore))
+ (wttrin--mode-line-stop)
+ (should-not wttrin--buffer-refresh-timer))
+ (test-wttrin--mode-line-stop-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin--mode-line-stop-boundary-safe-when-already-stopped ()
+ "Calling stop when everything is already nil should not error."
+ (test-wttrin--mode-line-stop-setup)
+ (unwind-protect
+ (progn
+ ;; Everything is already nil from setup
+ (should-not wttrin--mode-line-timer)
+ (should-not wttrin--buffer-refresh-timer)
+ (should-not wttrin-mode-line-string)
+ (should-not wttrin--mode-line-cache)
+ ;; This should not error
+ (wttrin--mode-line-stop)
+ ;; Still nil
+ (should-not wttrin--mode-line-timer)
+ (should-not wttrin-mode-line-string))
+ (test-wttrin--mode-line-stop-teardown)))
+
+(provide 'test-wttrin--mode-line-stop)
+;;; test-wttrin--mode-line-stop.el ends here
diff --git a/tests/test-wttrin--save-debug-data.el b/tests/test-wttrin--save-debug-data.el
new file mode 100644
index 0000000..b480329
--- /dev/null
+++ b/tests/test-wttrin--save-debug-data.el
@@ -0,0 +1,144 @@
+;;; test-wttrin--save-debug-data.el --- Tests for wttrin--save-debug-data -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for wttrin--save-debug-data function.
+;; Tests that debug data files are created correctly with the expected contents.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;;; Setup and Teardown
+
+(defun test-wttrin--save-debug-data-setup ()
+ "Setup for save-debug-data tests."
+ (testutil-wttrin-setup))
+
+(defun test-wttrin--save-debug-data-teardown ()
+ "Teardown for save-debug-data tests."
+ (testutil-wttrin-teardown))
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin--save-debug-data-normal-creates-file-that-exists ()
+ "Returned filepath should point to a file that actually exists on disk."
+ (test-wttrin--save-debug-data-setup)
+ (unwind-protect
+ (let ((filepath (wttrin--save-debug-data "Paris" "some weather data")))
+ (unwind-protect
+ (should (file-exists-p filepath))
+ (when (file-exists-p filepath) (delete-file filepath))))
+ (test-wttrin--save-debug-data-teardown)))
+
+(ert-deftest test-wttrin--save-debug-data-normal-file-contains-location ()
+ "File should contain a Location header with the queried location."
+ (test-wttrin--save-debug-data-setup)
+ (unwind-protect
+ (let ((filepath (wttrin--save-debug-data "New Orleans, LA" "weather")))
+ (unwind-protect
+ (let ((contents (with-temp-buffer
+ (insert-file-contents filepath)
+ (buffer-string))))
+ (should (string-match-p "^Location: New Orleans, LA$" contents)))
+ (when (file-exists-p filepath) (delete-file filepath))))
+ (test-wttrin--save-debug-data-teardown)))
+
+(ert-deftest test-wttrin--save-debug-data-normal-file-contains-unit-system ()
+ "File should record the wttrin-unit-system value at the time of capture."
+ (test-wttrin--save-debug-data-setup)
+ (unwind-protect
+ (let* ((wttrin-unit-system "m")
+ (filepath (wttrin--save-debug-data "Paris" "weather data")))
+ (unwind-protect
+ (let ((contents (with-temp-buffer
+ (insert-file-contents filepath)
+ (buffer-string))))
+ (should (string-match-p "wttrin-unit-system: m" contents)))
+ (when (file-exists-p filepath) (delete-file filepath))))
+ (test-wttrin--save-debug-data-teardown)))
+
+(ert-deftest test-wttrin--save-debug-data-normal-file-contains-raw-response ()
+ "File should contain the raw weather response body after the separator."
+ (test-wttrin--save-debug-data-setup)
+ (unwind-protect
+ (let ((filepath (wttrin--save-debug-data "Berlin" "Clear skies 72°F")))
+ (unwind-protect
+ (let ((contents (with-temp-buffer
+ (insert-file-contents filepath)
+ (buffer-string))))
+ (should (string-match-p "--- Raw Response ---" contents))
+ (should (string-match-p "Clear skies 72°F" contents)))
+ (when (file-exists-p filepath) (delete-file filepath))))
+ (test-wttrin--save-debug-data-teardown)))
+
+(ert-deftest test-wttrin--save-debug-data-normal-file-contains-timestamp ()
+ "File should contain a human-readable timestamp."
+ (test-wttrin--save-debug-data-setup)
+ (unwind-protect
+ (let ((filepath (wttrin--save-debug-data "Tokyo" "data")))
+ (unwind-protect
+ (let ((contents (with-temp-buffer
+ (insert-file-contents filepath)
+ (buffer-string))))
+ ;; Timestamp should look like YYYY-MM-DD HH:MM:SS
+ (should (string-match-p "^Timestamp: [0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\} [0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\}$"
+ contents)))
+ (when (file-exists-p filepath) (delete-file filepath))))
+ (test-wttrin--save-debug-data-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin--save-debug-data-boundary-unicode-location ()
+ "File should preserve unicode characters in the location name."
+ (test-wttrin--save-debug-data-setup)
+ (unwind-protect
+ (let ((filepath (wttrin--save-debug-data "São Paulo, BR" "data")))
+ (unwind-protect
+ (let ((contents (with-temp-buffer
+ (insert-file-contents filepath)
+ (buffer-string))))
+ (should (string-match-p "São Paulo" contents)))
+ (when (file-exists-p filepath) (delete-file filepath))))
+ (test-wttrin--save-debug-data-teardown)))
+
+(ert-deftest test-wttrin--save-debug-data-boundary-empty-raw-string ()
+ "Empty raw-string should still produce a valid file (just no response body)."
+ (test-wttrin--save-debug-data-setup)
+ (unwind-protect
+ (let ((filepath (wttrin--save-debug-data "Paris" "")))
+ (unwind-protect
+ (progn
+ (should (file-exists-p filepath))
+ (let ((contents (with-temp-buffer
+ (insert-file-contents filepath)
+ (buffer-string))))
+ ;; Should still have the headers and separator
+ (should (string-match-p "Location: Paris" contents))
+ (should (string-match-p "--- Raw Response ---" contents))))
+ (when (file-exists-p filepath) (delete-file filepath))))
+ (test-wttrin--save-debug-data-teardown)))
+
+;;; Error Cases
+
+(ert-deftest test-wttrin--save-debug-data-error-nil-raw-string-should-not-crash ()
+ "Nil raw-string should be handled gracefully, not cause an insert error.
+This can happen when wttrin--display-weather is called with nil data
+and debug mode is on — save-debug-data is called before validation."
+ (test-wttrin--save-debug-data-setup)
+ (unwind-protect
+ ;; wttrin--save-debug-data calls (insert raw-string) which errors on nil.
+ ;; This test documents the bug: a nil raw-string should produce a file
+ ;; with an empty or placeholder response body, not crash.
+ (let ((filepath (wttrin--save-debug-data "Paris" nil)))
+ (unwind-protect
+ (should (file-exists-p filepath))
+ (when (and filepath (file-exists-p filepath)) (delete-file filepath))))
+ (test-wttrin--save-debug-data-teardown)))
+
+(provide 'test-wttrin--save-debug-data)
+;;; test-wttrin--save-debug-data.el ends here
diff --git a/tests/test-wttrin-clear-cache.el b/tests/test-wttrin-clear-cache.el
new file mode 100644
index 0000000..3404cf8
--- /dev/null
+++ b/tests/test-wttrin-clear-cache.el
@@ -0,0 +1,69 @@
+;;; test-wttrin-clear-cache.el --- Tests for wttrin-clear-cache -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for wttrin-clear-cache function.
+;; Tests the interactive command that clears all cached weather data.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;;; Setup and Teardown
+
+(defun test-wttrin-clear-cache-setup ()
+ "Setup for clear-cache tests."
+ (testutil-wttrin-setup))
+
+(defun test-wttrin-clear-cache-teardown ()
+ "Teardown for clear-cache tests."
+ (testutil-wttrin-teardown))
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin-clear-cache-normal-empties-all-entries ()
+ "Clearing the cache should remove all stored weather entries."
+ (test-wttrin-clear-cache-setup)
+ (unwind-protect
+ (progn
+ ;; Populate cache with several entries
+ (testutil-wttrin-add-to-cache "Paris" "data1")
+ (testutil-wttrin-add-to-cache "London" "data2")
+ (testutil-wttrin-add-to-cache "Tokyo" "data3")
+ (should (= 3 (testutil-wttrin-cache-size)))
+ (wttrin-clear-cache)
+ (should (= 0 (testutil-wttrin-cache-size))))
+ (test-wttrin-clear-cache-teardown)))
+
+(ert-deftest test-wttrin-clear-cache-normal-shows-confirmation ()
+ "User should be told the cache was cleared."
+ (test-wttrin-clear-cache-setup)
+ (unwind-protect
+ (let ((displayed-message nil))
+ (cl-letf (((symbol-function 'message)
+ (lambda (fmt &rest args)
+ (setq displayed-message (apply #'format fmt args)))))
+ (wttrin-clear-cache)
+ (should displayed-message)
+ (should (string-match-p "cache cleared" displayed-message))))
+ (test-wttrin-clear-cache-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin-clear-cache-boundary-already-empty ()
+ "Clearing an already empty cache should not error."
+ (test-wttrin-clear-cache-setup)
+ (unwind-protect
+ (progn
+ (should (= 0 (testutil-wttrin-cache-size)))
+ ;; Should not error
+ (wttrin-clear-cache)
+ (should (= 0 (testutil-wttrin-cache-size))))
+ (test-wttrin-clear-cache-teardown)))
+
+(provide 'test-wttrin-clear-cache)
+;;; test-wttrin-clear-cache.el ends here
diff --git a/tests/test-wttrin-fetch-raw-string.el b/tests/test-wttrin-fetch-raw-string.el
new file mode 100644
index 0000000..963fd29
--- /dev/null
+++ b/tests/test-wttrin-fetch-raw-string.el
@@ -0,0 +1,65 @@
+;;; test-wttrin-fetch-raw-string.el --- Tests for wttrin-fetch-raw-string -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for wttrin-fetch-raw-string function.
+;; Tests the public API for fetching weather data by location query.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;;; Setup and Teardown
+
+(defun test-wttrin-fetch-raw-string-setup ()
+ "Setup for fetch-raw-string tests."
+ (testutil-wttrin-setup))
+
+(defun test-wttrin-fetch-raw-string-teardown ()
+ "Teardown for fetch-raw-string tests."
+ (testutil-wttrin-teardown))
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin-fetch-raw-string-normal-builds-correct-url ()
+ "The fetch should use a properly constructed wttr.in URL for the query."
+ (test-wttrin-fetch-raw-string-setup)
+ (unwind-protect
+ (let ((fetched-url nil))
+ (cl-letf (((symbol-function 'wttrin--fetch-url)
+ (lambda (url _callback) (setq fetched-url url))))
+ (wttrin-fetch-raw-string "Paris" #'ignore)
+ ;; URL should contain wttr.in and the encoded location
+ (should (string-match-p "wttr\\.in" fetched-url))
+ (should (string-match-p "Paris" fetched-url))))
+ (test-wttrin-fetch-raw-string-teardown)))
+
+(ert-deftest test-wttrin-fetch-raw-string-normal-passes-callback-through ()
+ "The user's callback should receive the fetched data."
+ (test-wttrin-fetch-raw-string-setup)
+ (unwind-protect
+ (let ((received-data nil))
+ (cl-letf (((symbol-function 'wttrin--fetch-url)
+ (lambda (_url callback)
+ (funcall callback "weather response"))))
+ (wttrin-fetch-raw-string "Paris"
+ (lambda (data) (setq received-data data)))
+ (should (equal received-data "weather response"))))
+ (test-wttrin-fetch-raw-string-teardown)))
+
+;;; Error Cases
+
+(ert-deftest test-wttrin-fetch-raw-string-error-nil-query-signals-error ()
+ "Passing nil as query should signal an error (invalid URL construction)."
+ (test-wttrin-fetch-raw-string-setup)
+ (unwind-protect
+ (should-error (wttrin-fetch-raw-string nil #'ignore)
+ :type 'error)
+ (test-wttrin-fetch-raw-string-teardown)))
+
+(provide 'test-wttrin-fetch-raw-string)
+;;; test-wttrin-fetch-raw-string.el ends here
diff --git a/tests/test-wttrin-mode-line-click.el b/tests/test-wttrin-mode-line-click.el
new file mode 100644
index 0000000..fccd45e
--- /dev/null
+++ b/tests/test-wttrin-mode-line-click.el
@@ -0,0 +1,69 @@
+;;; test-wttrin-mode-line-click.el --- Tests for wttrin-mode-line-click -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for wttrin-mode-line-click function.
+;; Tests the left-click handler for the mode-line weather widget.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;;; Setup and Teardown
+
+(defun test-wttrin-mode-line-click-setup ()
+ "Setup for mode-line-click tests."
+ (testutil-wttrin-setup))
+
+(defun test-wttrin-mode-line-click-teardown ()
+ "Teardown for mode-line-click tests."
+ (testutil-wttrin-teardown)
+ (when (get-buffer "*wttr.in*")
+ (kill-buffer "*wttr.in*")))
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin-mode-line-click-normal-opens-weather-for-favorite ()
+ "Clicking the mode-line widget should open weather for the favorite location."
+ (test-wttrin-mode-line-click-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "New Orleans, LA")
+ (opened-location nil))
+ (cl-letf (((symbol-function 'wttrin)
+ (lambda (location) (setq opened-location location))))
+ (wttrin-mode-line-click)
+ (should (equal opened-location "New Orleans, LA"))))
+ (test-wttrin-mode-line-click-teardown)))
+
+(ert-deftest test-wttrin-mode-line-click-normal-passes-exact-location-string ()
+ "The exact favorite location string should be passed to wttrin, not modified."
+ (test-wttrin-mode-line-click-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "São Paulo, BR")
+ (opened-location nil))
+ (cl-letf (((symbol-function 'wttrin)
+ (lambda (location) (setq opened-location location))))
+ (wttrin-mode-line-click)
+ (should (equal opened-location "São Paulo, BR"))))
+ (test-wttrin-mode-line-click-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin-mode-line-click-boundary-nil-location-is-noop ()
+ "When no favorite location is configured, clicking should do nothing."
+ (test-wttrin-mode-line-click-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location nil)
+ (wttrin-called nil))
+ (cl-letf (((symbol-function 'wttrin)
+ (lambda (_location) (setq wttrin-called t))))
+ (wttrin-mode-line-click)
+ (should-not wttrin-called)))
+ (test-wttrin-mode-line-click-teardown)))
+
+(provide 'test-wttrin-mode-line-click)
+;;; test-wttrin-mode-line-click.el ends here
diff --git a/tests/test-wttrin-mode-line-force-refresh.el b/tests/test-wttrin-mode-line-force-refresh.el
new file mode 100644
index 0000000..2275cee
--- /dev/null
+++ b/tests/test-wttrin-mode-line-force-refresh.el
@@ -0,0 +1,71 @@
+;;; test-wttrin-mode-line-force-refresh.el --- Tests for wttrin-mode-line-force-refresh -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for wttrin-mode-line-force-refresh function.
+;; Tests the right-click handler that force-refreshes mode-line weather.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;;; Setup and Teardown
+
+(defun test-wttrin-mode-line-force-refresh-setup ()
+ "Setup for mode-line-force-refresh tests."
+ (testutil-wttrin-setup)
+ (setq wttrin-mode-line-string nil)
+ (setq wttrin--mode-line-cache nil))
+
+(defun test-wttrin-mode-line-force-refresh-teardown ()
+ "Teardown for mode-line-force-refresh tests."
+ (testutil-wttrin-teardown)
+ (setq wttrin-mode-line-string nil)
+ (setq wttrin--mode-line-cache nil))
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin-mode-line-force-refresh-normal-calls-fetch ()
+ "Right-click should trigger a weather fetch."
+ (test-wttrin-mode-line-force-refresh-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris")
+ (fetch-called nil))
+ (cl-letf (((symbol-function 'wttrin--mode-line-fetch-weather)
+ (lambda () (setq fetch-called t))))
+ (wttrin-mode-line-force-refresh)
+ (should fetch-called)))
+ (test-wttrin-mode-line-force-refresh-teardown)))
+
+(ert-deftest test-wttrin-mode-line-force-refresh-normal-sets-force-flag ()
+ "The fetch should run with force-refresh bound to t to bypass cache."
+ (test-wttrin-mode-line-force-refresh-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location "Paris")
+ (force-flag-during-fetch nil))
+ (cl-letf (((symbol-function 'wttrin--mode-line-fetch-weather)
+ (lambda () (setq force-flag-during-fetch wttrin--force-refresh))))
+ (wttrin-mode-line-force-refresh)
+ (should force-flag-during-fetch)))
+ (test-wttrin-mode-line-force-refresh-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin-mode-line-force-refresh-boundary-nil-location-is-noop ()
+ "When no favorite location is set, right-click should do nothing."
+ (test-wttrin-mode-line-force-refresh-setup)
+ (unwind-protect
+ (let ((wttrin-favorite-location nil)
+ (fetch-called nil))
+ (cl-letf (((symbol-function 'wttrin--mode-line-fetch-weather)
+ (lambda () (setq fetch-called t))))
+ (wttrin-mode-line-force-refresh)
+ (should-not fetch-called)))
+ (test-wttrin-mode-line-force-refresh-teardown)))
+
+(provide 'test-wttrin-mode-line-force-refresh)
+;;; test-wttrin-mode-line-force-refresh.el ends here
diff --git a/tests/test-wttrin-query.el b/tests/test-wttrin-query.el
new file mode 100644
index 0000000..a44040a
--- /dev/null
+++ b/tests/test-wttrin-query.el
@@ -0,0 +1,122 @@
+;;; test-wttrin-query.el --- Tests for wttrin-query -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for wttrin-query function.
+;; Tests the async weather query orchestration: buffer creation,
+;; loading state, cache lookup, and display callback.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;;; Setup and Teardown
+
+(defun test-wttrin-query-setup ()
+ "Setup for wttrin-query tests."
+ (testutil-wttrin-setup)
+ (when (get-buffer "*wttr.in*")
+ (kill-buffer "*wttr.in*")))
+
+(defun test-wttrin-query-teardown ()
+ "Teardown for wttrin-query tests."
+ (testutil-wttrin-teardown)
+ (when (get-buffer "*wttr.in*")
+ (kill-buffer "*wttr.in*")))
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin-query-normal-creates-buffer ()
+ "Calling query should create the *wttr.in* buffer."
+ (test-wttrin-query-setup)
+ (unwind-protect
+ (cl-letf (((symbol-function 'wttrin--get-cached-or-fetch)
+ (lambda (_location _callback) nil)))
+ (wttrin-query "Paris")
+ (should (get-buffer "*wttr.in*")))
+ (test-wttrin-query-teardown)))
+
+(ert-deftest test-wttrin-query-normal-shows-loading-message ()
+ "Buffer should show a loading message with the location name while fetching."
+ (test-wttrin-query-setup)
+ (unwind-protect
+ (cl-letf (((symbol-function 'wttrin--get-cached-or-fetch)
+ (lambda (_location _callback) nil)))
+ (wttrin-query "New Orleans, LA")
+ (with-current-buffer "*wttr.in*"
+ (let ((contents (buffer-string)))
+ (should (string-match-p "Loading" contents))
+ (should (string-match-p "New Orleans, LA" contents)))))
+ (test-wttrin-query-teardown)))
+
+(ert-deftest test-wttrin-query-normal-buffer-is-read-only-during-loading ()
+ "The loading buffer should be read-only to prevent user edits."
+ (test-wttrin-query-setup)
+ (unwind-protect
+ (cl-letf (((symbol-function 'wttrin--get-cached-or-fetch)
+ (lambda (_location _callback) nil)))
+ (wttrin-query "Tokyo")
+ (with-current-buffer "*wttr.in*"
+ (should buffer-read-only)))
+ (test-wttrin-query-teardown)))
+
+(ert-deftest test-wttrin-query-normal-fetches-for-correct-location ()
+ "Query should request weather for the specified location."
+ (test-wttrin-query-setup)
+ (unwind-protect
+ (let ((fetched-location nil))
+ (cl-letf (((symbol-function 'wttrin--get-cached-or-fetch)
+ (lambda (location _callback)
+ (setq fetched-location location))))
+ (wttrin-query "Berlin, DE")
+ (should (equal fetched-location "Berlin, DE"))))
+ (test-wttrin-query-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin-query-boundary-dead-buffer-callback-is-safe ()
+ "If the buffer is killed before the async callback fires, it should not error."
+ (test-wttrin-query-setup)
+ (unwind-protect
+ (let ((saved-callback nil))
+ (cl-letf (((symbol-function 'wttrin--get-cached-or-fetch)
+ (lambda (_location callback)
+ (setq saved-callback callback))))
+ (wttrin-query "Paris")
+ ;; Kill the buffer before callback fires
+ (kill-buffer "*wttr.in*")
+ ;; Invoke callback — should not error
+ (should-not (get-buffer "*wttr.in*"))
+ (funcall saved-callback "weather data")
+ ;; Buffer should NOT be recreated
+ (should-not (get-buffer "*wttr.in*"))))
+ (test-wttrin-query-teardown)))
+
+;;; Error Cases
+
+(ert-deftest test-wttrin-query-error-nil-response-shows-error-message ()
+ "When fetch returns nil, the user should see an error message, not a crash."
+ (test-wttrin-query-setup)
+ (unwind-protect
+ (let ((saved-callback nil)
+ (displayed-message nil))
+ (cl-letf (((symbol-function 'wttrin--get-cached-or-fetch)
+ (lambda (_location callback)
+ (setq saved-callback callback)))
+ ((symbol-function 'message)
+ (lambda (fmt &rest args)
+ (setq displayed-message (apply #'format fmt args)))))
+ (wttrin-query "BadLocation")
+ ;; Simulate fetch returning nil
+ (funcall saved-callback nil)
+ ;; Should have shown error message (from wttrin--display-weather validation)
+ (should displayed-message)
+ (should (string-match-p "Cannot retrieve" displayed-message))))
+ (test-wttrin-query-teardown)))
+
+(provide 'test-wttrin-query)
+;;; test-wttrin-query.el ends here
diff --git a/tests/test-wttrin-requery-force.el b/tests/test-wttrin-requery-force.el
new file mode 100644
index 0000000..04ad40a
--- /dev/null
+++ b/tests/test-wttrin-requery-force.el
@@ -0,0 +1,91 @@
+;;; test-wttrin-requery-force.el --- Tests for wttrin-requery-force -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for wttrin-requery-force function.
+;; Tests the force-refresh behavior that bypasses cache for the current location.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;;; Setup and Teardown
+
+(defun test-wttrin-requery-force-setup ()
+ "Setup for requery-force tests."
+ (testutil-wttrin-setup)
+ (when (get-buffer "*wttr.in*")
+ (kill-buffer "*wttr.in*")))
+
+(defun test-wttrin-requery-force-teardown ()
+ "Teardown for requery-force tests."
+ (testutil-wttrin-teardown)
+ (when (get-buffer "*wttr.in*")
+ (kill-buffer "*wttr.in*")))
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin-requery-force-normal-queries-current-location ()
+ "Force refresh should query weather for the buffer's current location."
+ (test-wttrin-requery-force-setup)
+ (unwind-protect
+ (let ((queried-location nil))
+ (cl-letf (((symbol-function 'wttrin-query)
+ (lambda (location) (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")
+ (wttrin-requery-force)
+ (should (equal queried-location "Berlin, DE")))))
+ (test-wttrin-requery-force-teardown)))
+
+(ert-deftest test-wttrin-requery-force-normal-sets-force-refresh-flag ()
+ "The force-refresh flag should be true when the query executes."
+ (test-wttrin-requery-force-setup)
+ (unwind-protect
+ (let ((force-refresh-was-set nil))
+ (cl-letf (((symbol-function 'wttrin-query)
+ (lambda (_location)
+ (setq force-refresh-was-set wttrin--force-refresh))))
+ (with-current-buffer (get-buffer-create "*wttr.in*")
+ (setq-local wttrin--current-location "Paris")
+ (wttrin-requery-force)
+ (should force-refresh-was-set))))
+ (test-wttrin-requery-force-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin-requery-force-boundary-no-location-shows-message ()
+ "When no current location is set, user should be told there's nothing to refresh."
+ (test-wttrin-requery-force-setup)
+ (unwind-protect
+ (let ((displayed-message nil))
+ (cl-letf (((symbol-function 'message)
+ (lambda (fmt &rest args)
+ (setq displayed-message (apply #'format fmt args)))))
+ (with-current-buffer (get-buffer-create "*wttr.in*")
+ ;; wttrin--current-location is nil (buffer-local default)
+ (wttrin-requery-force)
+ (should displayed-message)
+ (should (string-match-p "No location" displayed-message)))))
+ (test-wttrin-requery-force-teardown)))
+
+(ert-deftest test-wttrin-requery-force-boundary-force-flag-does-not-leak ()
+ "The force-refresh flag should not persist after the requery completes."
+ (test-wttrin-requery-force-setup)
+ (unwind-protect
+ (progn
+ (cl-letf (((symbol-function 'wttrin-query) (lambda (_location) nil)))
+ (with-current-buffer (get-buffer-create "*wttr.in*")
+ (setq-local wttrin--current-location "Paris")
+ (wttrin-requery-force)))
+ ;; After the let-binding unwinds, force-refresh should be back to nil
+ (should-not wttrin--force-refresh))
+ (test-wttrin-requery-force-teardown)))
+
+(provide 'test-wttrin-requery-force)
+;;; test-wttrin-requery-force.el ends here
diff --git a/tests/test-wttrin-requery.el b/tests/test-wttrin-requery.el
new file mode 100644
index 0000000..e643a34
--- /dev/null
+++ b/tests/test-wttrin-requery.el
@@ -0,0 +1,152 @@
+;;; test-wttrin-requery.el --- Tests for wttrin-requery -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for wttrin--requery-location and wttrin-requery.
+;; wttrin--requery-location holds the core logic (kill buffer, query new location).
+;; wttrin-requery is the interactive wrapper that adds completing-read.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+(require 'testutil-wttrin)
+
+;;; Setup and Teardown
+
+(defun test-wttrin-requery-setup ()
+ "Setup for requery tests."
+ (testutil-wttrin-setup)
+ (when (get-buffer "*wttr.in*")
+ (kill-buffer "*wttr.in*")))
+
+(defun test-wttrin-requery-teardown ()
+ "Teardown for requery tests."
+ (testutil-wttrin-teardown)
+ (when (get-buffer "*wttr.in*")
+ (kill-buffer "*wttr.in*")))
+
+;;; --------------------------------------------------------------------------
+;;; wttrin--requery-location (core logic)
+;;; --------------------------------------------------------------------------
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin--requery-location-normal-kills-existing-buffer ()
+ "Requerying should kill the existing *wttr.in* buffer before opening a new one."
+ (test-wttrin-requery-setup)
+ (unwind-protect
+ (let ((old-buffer (get-buffer-create "*wttr.in*")))
+ (cl-letf (((symbol-function 'wttrin-query) (lambda (_loc) nil)))
+ (wttrin--requery-location "Tokyo")
+ ;; Old buffer should be dead
+ (should-not (buffer-live-p old-buffer))))
+ (test-wttrin-requery-teardown)))
+
+(ert-deftest test-wttrin--requery-location-normal-queries-new-location ()
+ "Requerying should fetch weather for the newly specified location."
+ (test-wttrin-requery-setup)
+ (unwind-protect
+ (let ((queried-location nil))
+ (cl-letf (((symbol-function 'wttrin-query)
+ (lambda (loc) (setq queried-location loc))))
+ (wttrin--requery-location "Berlin, DE")
+ (should (equal queried-location "Berlin, DE"))))
+ (test-wttrin-requery-teardown)))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin--requery-location-boundary-no-existing-buffer ()
+ "Requerying when no weather buffer exists should still query the new location."
+ (test-wttrin-requery-setup)
+ (unwind-protect
+ (let ((queried-location nil))
+ ;; Ensure no buffer exists
+ (should-not (get-buffer "*wttr.in*"))
+ (cl-letf (((symbol-function 'wttrin-query)
+ (lambda (loc) (setq queried-location loc))))
+ (wttrin--requery-location "Paris")
+ (should (equal queried-location "Paris"))))
+ (test-wttrin-requery-teardown)))
+
+(ert-deftest test-wttrin--requery-location-boundary-unicode-location ()
+ "Requerying with unicode characters should pass them through unchanged."
+ (test-wttrin-requery-setup)
+ (unwind-protect
+ (let ((queried-location nil))
+ (cl-letf (((symbol-function 'wttrin-query)
+ (lambda (loc) (setq queried-location loc))))
+ (wttrin--requery-location "Zürich, CH")
+ (should (equal queried-location "Zürich, CH"))))
+ (test-wttrin-requery-teardown)))
+
+;;; --------------------------------------------------------------------------
+;;; wttrin-requery (interactive wrapper)
+;;; --------------------------------------------------------------------------
+
+(ert-deftest test-wttrin-requery-normal-uses-selected-location ()
+ "The interactive command should pass the user's completing-read selection
+to the core requery function."
+ (test-wttrin-requery-setup)
+ (unwind-protect
+ (let ((requeried-location nil))
+ (cl-letf (((symbol-function 'completing-read)
+ (lambda (_prompt _collection &rest _args) "London, GB"))
+ ((symbol-function 'wttrin--requery-location)
+ (lambda (loc) (setq requeried-location loc))))
+ (wttrin-requery)
+ (should (equal requeried-location "London, GB"))))
+ (test-wttrin-requery-teardown)))
+
+(ert-deftest test-wttrin-requery-normal-offers-default-locations ()
+ "Completing-read should be called with wttrin-default-locations as candidates."
+ (test-wttrin-requery-setup)
+ (unwind-protect
+ (let ((offered-collection nil)
+ (wttrin-default-locations '("Paris" "London" "Tokyo")))
+ (cl-letf (((symbol-function 'completing-read)
+ (lambda (_prompt collection &rest _args)
+ (setq offered-collection collection)
+ "Paris"))
+ ((symbol-function 'wttrin--requery-location)
+ (lambda (_loc) nil)))
+ (wttrin-requery)
+ (should (equal offered-collection '("Paris" "London" "Tokyo")))))
+ (test-wttrin-requery-teardown)))
+
+(ert-deftest test-wttrin-requery-boundary-single-default-prefills ()
+ "When only one default location exists, it should be pre-filled in the prompt."
+ (test-wttrin-requery-setup)
+ (unwind-protect
+ (let ((initial-input nil)
+ (wttrin-default-locations '("Solo City")))
+ (cl-letf (((symbol-function 'completing-read)
+ (lambda (_prompt _collection _predicate _require-match init &rest _args)
+ (setq initial-input init)
+ "Solo City"))
+ ((symbol-function 'wttrin--requery-location)
+ (lambda (_loc) nil)))
+ (wttrin-requery)
+ (should (equal initial-input "Solo City"))))
+ (test-wttrin-requery-teardown)))
+
+(ert-deftest test-wttrin-requery-boundary-multiple-defaults-no-prefill ()
+ "When multiple default locations exist, nothing should be pre-filled."
+ (test-wttrin-requery-setup)
+ (unwind-protect
+ (let ((initial-input 'not-called)
+ (wttrin-default-locations '("Paris" "London")))
+ (cl-letf (((symbol-function 'completing-read)
+ (lambda (_prompt _collection _predicate _require-match init &rest _args)
+ (setq initial-input init)
+ "Paris"))
+ ((symbol-function 'wttrin--requery-location)
+ (lambda (_loc) nil)))
+ (wttrin-requery)
+ (should-not initial-input)))
+ (test-wttrin-requery-teardown)))
+
+(provide 'test-wttrin-requery)
+;;; test-wttrin-requery.el ends here