diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-01 22:57:03 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-01 22:57:03 -0400 |
| commit | 6c808ff4a4c54d543e065b722bc8517fe5141b45 (patch) | |
| tree | 5519b1dfeb9c798acb466bebbfe43807f9cc9911 /tests | |
| parent | ad22b39fb5661a6662865c0e88dcfb0d1627eeb4 (diff) | |
| download | emacs-wttrin-6c808ff4a4c54d543e065b722bc8517fe5141b45.tar.gz emacs-wttrin-6c808ff4a4c54d543e065b722bc8517fe5141b45.zip | |
feat(mode-line): optional multi-day forecast in the weather tooltip
The tooltip showed current conditions only. I added wttrin-mode-line-tooltip-forecast-days (default 0, capped at wttr.in's three days). When positive, the mode-line refresh also fetches ?format=j1 and caches the parsed day list, and the tooltip renders one line per day between the conditions and the age line: Today / Tomorrow / weekday label, min-max temps in the configured unit, and the midday description. A failed or malformed forecast fetch keeps the previous forecast and never disturbs the main fetch.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-wttrin--mode-line-forecast.el | 149 |
1 files changed, 149 insertions, 0 deletions
diff --git a/tests/test-wttrin--mode-line-forecast.el b/tests/test-wttrin--mode-line-forecast.el new file mode 100644 index 0000000..c371f94 --- /dev/null +++ b/tests/test-wttrin--mode-line-forecast.el @@ -0,0 +1,149 @@ +;;; test-wttrin--mode-line-forecast.el --- Tests for the tooltip forecast -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Craig Jennings + +;;; Commentary: + +;; Unit tests for the mode-line tooltip forecast: parsing wttr.in's j1 JSON +;; into a day list, formatting up to `wttrin-mode-line-tooltip-forecast-days' +;; lines, and appending the block to `wttrin--mode-line-tooltip' output. + +;;; Code: + +(require 'ert) +(require 'wttrin) +(require 'testutil-wttrin) + +;;; Fixture + +(defconst test-wttrin--forecast-j1 + "{\"weather\": [ + {\"date\": \"2026-07-01\", \"mintempC\": \"22\", \"maxtempC\": \"31\", + \"mintempF\": \"71\", \"maxtempF\": \"88\", + \"hourly\": [{\"time\": \"0\", \"weatherDesc\": [{\"value\": \"Clear\"}]}, + {\"time\": \"1200\", \"weatherDesc\": [{\"value\": \"Partly cloudy\"}]}, + {\"time\": \"2100\", \"weatherDesc\": [{\"value\": \"Clear\"}]}]}, + {\"date\": \"2026-07-02\", \"mintempC\": \"20\", \"maxtempC\": \"26\", + \"mintempF\": \"68\", \"maxtempF\": \"79\", + \"hourly\": [{\"time\": \"1200\", \"weatherDesc\": [{\"value\": \"Light rain\"}]}]}, + {\"date\": \"2026-07-03\", \"mintempC\": \"21\", \"maxtempC\": \"31\", + \"mintempF\": \"70\", \"maxtempF\": \"88\", + \"hourly\": [{\"time\": \"1200\", \"weatherDesc\": [{\"value\": \"Sunny\"}]}]}]}" + "Trimmed wttr.in ?format=j1 response: three days, the keys the code reads.") + +(defun test-wttrin--forecast-days () + "Return the parsed day list from the fixture." + (wttrin--forecast-parse test-wttrin--forecast-j1)) + +;;; -------------------------------------------------------------------------- +;;; wttrin--forecast-parse +;;; -------------------------------------------------------------------------- + +(ert-deftest test-wttrin--forecast-parse-normal-three-days () + "Normal: the fixture parses into a list of three day alists." + (let ((days (test-wttrin--forecast-days))) + (should (= 3 (length days))) + (should (equal "2026-07-01" (alist-get 'date (car days)))))) + +(ert-deftest test-wttrin--forecast-parse-error-malformed-json () + "Error: malformed JSON returns nil instead of signaling." + (should-not (wttrin--forecast-parse "{not json"))) + +(ert-deftest test-wttrin--forecast-parse-error-missing-weather-key () + "Error: JSON without a weather array returns nil." + (should-not (wttrin--forecast-parse "{\"nearest_area\": []}"))) + +(ert-deftest test-wttrin--forecast-parse-boundary-nil-and-empty () + "Boundary: nil and empty-string input return nil." + (should-not (wttrin--forecast-parse nil)) + (should-not (wttrin--forecast-parse ""))) + +;;; -------------------------------------------------------------------------- +;;; wttrin--forecast-format +;;; -------------------------------------------------------------------------- + +(ert-deftest test-wttrin--forecast-format-normal-three-days-fahrenheit () + "Normal: unit system \"u\" renders three labeled lines with °F ranges." + (let* ((wttrin-unit-system "u") + (text (wttrin--forecast-format (test-wttrin--forecast-days) 3)) + (lines (split-string text "\n"))) + (should (= 3 (length lines))) + (should (string-match-p "\\`Today 71-88°F Partly cloudy\\'" (nth 0 lines))) + (should (string-match-p "\\`Tomorrow 68-79°F Light rain\\'" (nth 1 lines))) + ;; 2026-07-03 is a Friday; the third day is labeled by weekday. + (should (string-match-p "\\`Fri 70-88°F Sunny\\'" (nth 2 lines))))) + +(ert-deftest test-wttrin--forecast-format-normal-metric () + "Normal: a non-\"u\" unit system renders °C ranges." + (let* ((wttrin-unit-system "m") + (text (wttrin--forecast-format (test-wttrin--forecast-days) 2))) + (should (string-match-p "Today 22-31°C" text)) + (should (string-match-p "Tomorrow 20-26°C" text)))) + +(ert-deftest test-wttrin--forecast-format-boundary-day-counts () + "Boundary: 0 days is nil; 1 day is one line; >3 caps at the data." + (let ((wttrin-unit-system "u") + (days (test-wttrin--forecast-days))) + (should-not (wttrin--forecast-format days 0)) + (should (= 1 (length (split-string (wttrin--forecast-format days 1) "\n")))) + (should (= 3 (length (split-string (wttrin--forecast-format days 5) "\n")))))) + +(ert-deftest test-wttrin--forecast-format-boundary-missing-hourly-desc () + "Boundary: a day with no hourly data renders the temps without a description." + (let* ((wttrin-unit-system "u") + (day '((date . "2026-07-01") (mintempF . "71") (maxtempF . "88") + (mintempC . "22") (maxtempC . "31") (hourly . ()))) + (text (wttrin--forecast-format (list day) 1))) + (should (equal "Today 71-88°F" text)))) + +(ert-deftest test-wttrin--forecast-format-error-nil-days () + "Error: nil day list returns nil." + (should-not (wttrin--forecast-format nil 3))) + +;;; -------------------------------------------------------------------------- +;;; tooltip integration +;;; -------------------------------------------------------------------------- + +(defun test-wttrin--forecast-tooltip-fixture (option-days) + "Return the tooltip with fresh current + forecast caches and OPTION-DAYS." + (let ((wttrin-unit-system "u") + (wttrin-mode-line-tooltip-forecast-days option-days) + (wttrin--mode-line-cache (cons (float-time) "Paris: ☀️ +61°F Clear")) + (wttrin--mode-line-forecast-cache + (cons (float-time) (test-wttrin--forecast-days)))) + (wttrin--mode-line-tooltip))) + +(ert-deftest test-wttrin--forecast-tooltip-normal-appends-block () + "Normal: with the option at 3, the tooltip carries the forecast lines." + (testutil-wttrin-setup) + (unwind-protect + (let ((tooltip (test-wttrin--forecast-tooltip-fixture 3))) + (should (string-match-p "Paris" tooltip)) + (should (string-match-p "Today 71-88°F Partly cloudy" tooltip)) + (should (string-match-p "Fri 70-88°F Sunny" tooltip)) + (should (string-match-p "Updated" tooltip))) + (testutil-wttrin-teardown))) + +(ert-deftest test-wttrin--forecast-tooltip-boundary-option-zero-unchanged () + "Boundary: with the option at 0 (default), the tooltip has no forecast." + (testutil-wttrin-setup) + (unwind-protect + (let ((tooltip (test-wttrin--forecast-tooltip-fixture 0))) + (should (string-match-p "Paris" tooltip)) + (should-not (string-match-p "Today" tooltip))) + (testutil-wttrin-teardown))) + +(ert-deftest test-wttrin--forecast-tooltip-boundary-empty-forecast-cache () + "Boundary: option on but no forecast cache yet leaves the tooltip as before." + (testutil-wttrin-setup) + (unwind-protect + (let ((wttrin-mode-line-tooltip-forecast-days 3) + (wttrin--mode-line-cache (cons (float-time) "Paris: ☀️ +61°F Clear")) + (wttrin--mode-line-forecast-cache nil)) + (let ((tooltip (wttrin--mode-line-tooltip))) + (should (string-match-p "Paris" tooltip)) + (should-not (string-match-p "Today" tooltip)))) + (testutil-wttrin-teardown))) + +(provide 'test-wttrin--mode-line-forecast) +;;; test-wttrin--mode-line-forecast.el ends here |
