1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
|
;;; test-wttrin-ansi-color-rendering.el --- Test ANSI color rendering -*- lexical-binding: t; -*-
;; Copyright (C) 2025 Craig Jennings
;;; Commentary:
;; Test that ANSI color codes are properly rendered in the weather buffer.
;; This reproduces the bug where clicking the mode-line icon shows white text.
;;; Code:
(require 'ert)
(require 'wttrin)
(require 'xterm-color)
(require 'testutil-wttrin)
;;; Test Data - Realistic wttr.in response with ANSI codes
(defconst test-wttrin-ansi-sample-with-colors
"Weather report: Paris
\x1b[38;5;226m \\ /\x1b[0m Partly cloudy
\x1b[38;5;226m _ /\"\"\x1b[38;5;250m.-.\x1b[0m \x1b[38;5;118m+13\x1b[0m(\x1b[38;5;082m12\x1b[0m) °C
\x1b[38;5;226m \\_\x1b[38;5;250m( ). \x1b[0m ↑ \x1b[38;5;190m12\x1b[0m km/h
"
"Sample weather data with ANSI color codes (escape sequences).")
;;; Test Setup and Teardown
(defun test-wttrin-ansi-setup ()
"Setup for ANSI color rendering tests."
(testutil-wttrin-setup)
(when (get-buffer "*wttr.in*")
(kill-buffer "*wttr.in*")))
(defun test-wttrin-ansi-teardown ()
"Teardown for ANSI color rendering tests."
(testutil-wttrin-teardown)
(when (get-buffer "*wttr.in*")
(kill-buffer "*wttr.in*")))
;;; Helper Functions
(defun test-wttrin-ansi--count-colored-text ()
"Count number of characters with color text properties in current buffer.
Returns cons (colored-chars . total-chars)."
(save-excursion
(goto-char (point-min))
(let ((colored 0)
(total 0))
(while (not (eobp))
(let ((props (text-properties-at (point))))
(setq total (1+ total))
;; Check for xterm-color face properties
(when (or (plist-get props 'face)
(plist-get props 'font-lock-face))
(setq colored (1+ colored))))
(forward-char 1))
(cons colored total))))
(defun test-wttrin-ansi--has-ansi-escape-codes (str)
"Check if STR contains ANSI escape codes."
(string-match-p "\x1b\\[" str))
;;; Tests
(ert-deftest test-wttrin-ansi-xterm-color-filter-processes-codes ()
"Test that xterm-color-filter properly processes ANSI codes."
(test-wttrin-ansi-setup)
(unwind-protect
(let ((filtered (xterm-color-filter test-wttrin-ansi-sample-with-colors)))
;; After filtering, ANSI escape codes should be removed
(should-not (test-wttrin-ansi--has-ansi-escape-codes filtered))
;; The filtered text should be shorter (escape codes removed)
(should (< (length filtered) (length test-wttrin-ansi-sample-with-colors)))
;; Text should still contain the actual weather content
(should (string-match-p "Paris" filtered))
(should (string-match-p "Partly cloudy" filtered)))
(test-wttrin-ansi-teardown)))
(ert-deftest test-wttrin-ansi-display-weather-renders-colors ()
"Test that display-weather properly renders ANSI colors in buffer."
(test-wttrin-ansi-setup)
(unwind-protect
(progn
(wttrin--display-weather "Paris" test-wttrin-ansi-sample-with-colors)
(should (get-buffer "*wttr.in*"))
(with-current-buffer "*wttr.in*"
;; Buffer should not contain raw ANSI escape codes
(let ((buffer-text (buffer-substring-no-properties (point-min) (point-max))))
(should-not (test-wttrin-ansi--has-ansi-escape-codes buffer-text)))
;; Buffer should contain the weather text
(goto-char (point-min))
(should (search-forward "Paris" nil t))
(should (search-forward "Partly cloudy" nil t))
;; Check that some text has color properties
(let* ((counts (test-wttrin-ansi--count-colored-text))
(colored (car counts))
(total (cdr counts)))
;; At least some characters should have color properties
;; (not all white text)
(should (> colored 0))
;; Colored text should be a reasonable portion (not just 2 chars)
;; With ANSI codes, we expect at least 10% of text to be colored
(should (> colored (/ total 10))))))
(test-wttrin-ansi-teardown)))
(ert-deftest test-wttrin-ansi-mode-line-click-renders-colors ()
"Test that clicking mode-line icon renders colors properly.
This reproduces the bug where mode-line click shows white text."
(test-wttrin-ansi-setup)
(unwind-protect
(let* ((location "Paris")
(cache-key (wttrin--make-cache-key location))
(now 1000.0)
(wttrin-mode-line-favorite-location location))
;; Mock the async fetch to return ANSI-coded data
(cl-letf (((symbol-function 'float-time)
(lambda () now))
((symbol-function 'wttrin-fetch-raw-string)
(lambda (_location callback)
(funcall callback test-wttrin-ansi-sample-with-colors)))
((symbol-function 'wttrin--cleanup-cache-if-needed)
(lambda () nil)))
;; Simulate mode-line click by calling wttrin
;; (wttrin-mode-line-click just calls this)
(wttrin location)
;; Give async callback time to execute
;; (In real execution this is async, but our mock is synchronous)
(should (get-buffer "*wttr.in*"))
(with-current-buffer "*wttr.in*"
;; Buffer should not contain raw ANSI codes
(let ((buffer-text (buffer-substring-no-properties (point-min) (point-max))))
(should-not (test-wttrin-ansi--has-ansi-escape-codes buffer-text)))
;; Check that text has color properties (not all white)
(let* ((counts (test-wttrin-ansi--count-colored-text))
(colored (car counts)))
;; Should have colored text (proving colors are rendered)
(should (> colored 0))))))
(test-wttrin-ansi-teardown)))
(ert-deftest test-wttrin-ansi-cached-data-preserves-colors ()
"Test that cached weather data preserves color information.
Verifies that cache doesn't strip ANSI codes or color properties."
(test-wttrin-ansi-setup)
(unwind-protect
(let* ((location "Berlin")
(cache-key (wttrin--make-cache-key location))
(now 1000.0))
;; Pre-populate cache with ANSI-coded data
(puthash cache-key (cons now test-wttrin-ansi-sample-with-colors)
wttrin--cache)
(cl-letf (((symbol-function 'float-time)
(lambda () (+ now 100.0)))) ; Within TTL
;; Call wttrin - should use cached data
(wttrin location)
(should (get-buffer "*wttr.in*"))
(with-current-buffer "*wttr.in*"
;; Even with cached data, colors should be rendered
(let* ((counts (test-wttrin-ansi--count-colored-text))
(colored (car counts))
(total (cdr counts)))
(should (> colored 0))
;; Should have reasonable color coverage
(should (> colored (/ total 10)))))))
(test-wttrin-ansi-teardown)))
(ert-deftest test-wttrin-ansi-buffer-face-mode-doesnt-strip-colors ()
"Test that buffer-face-mode doesn't strip xterm-color face properties.
This reproduces the bug where weather buffer shows mostly white text."
(test-wttrin-ansi-setup)
(unwind-protect
(progn
(wttrin--display-weather "Paris" test-wttrin-ansi-sample-with-colors)
(should (get-buffer "*wttr.in*"))
(with-current-buffer "*wttr.in*"
;; Check that face-remapping is active (for font customization)
(should face-remapping-alist)
;; But colors should still be present despite face remapping
(let* ((counts (test-wttrin-ansi--count-colored-text))
(colored (car counts))
(total (cdr counts))
(percentage (if (> total 0)
(* 100.0 (/ (float colored) total))
0)))
;; Should have substantial colored text (>10%)
;; If this fails with ~5% or less, buffer-face-mode is interfering
(should (> percentage 10.0))
;; With face-remap-add-relative fix, we get ~30-40 colored chars
;; (our test data is small, so absolute count is low)
;; The key is the percentage, not absolute count
(should (> colored 20)))))
(test-wttrin-ansi-teardown)))
(ert-deftest test-wttrin-ansi-mode-line-doesnt-pollute-main-cache ()
"Test that mode-line weather fetch doesn't pollute main cache with plain text.
This reproduces the bug where clicking mode-line shows white text."
(test-wttrin-ansi-setup)
(unwind-protect
(let* ((location "Berlin")
(cache-key (wttrin--make-cache-key location))
(now 1000.0)
(plain-text-weather "Berlin: ☀️ +20°C Clear") ; No ANSI codes
(ansi-weather test-wttrin-ansi-sample-with-colors)) ; Has ANSI codes
;; Simulate mode-line storing plain-text in cache (the bug)
;; This shouldn't happen, but let's verify the system is resilient
(puthash cache-key (cons now plain-text-weather) wttrin--cache)
(cl-letf (((symbol-function 'float-time)
(lambda () (+ now 100.0))) ; Within TTL, will use cache
((symbol-function 'wttrin-fetch-raw-string)
(lambda (_location callback)
;; Should never be called since cache is fresh
(error "Should not fetch when cache is fresh"))))
;; Call wttrin - it will use cached data
(wttrin location)
(should (get-buffer "*wttr.in*"))
(with-current-buffer "*wttr.in*"
;; Even if cache has plain text, should we handle it gracefully?
;; At minimum, buffer should exist and not error
(should (> (buffer-size) 0))
;; The real fix: mode-line should use separate cache or namespace
;; For now, document that plain-text cache = no colors
(let* ((buffer-text (buffer-substring-no-properties (point-min) (point-max))))
;; Should contain the weather data (even if not colored)
(should (string-match-p "Berlin" buffer-text))))))
(test-wttrin-ansi-teardown)))
(ert-deftest test-wttrin-ansi-full-scenario-mode-line-then-click ()
"Test full scenario: mode-line fetch, then user clicks to open buffer.
This is the exact user workflow that exposes the bug."
(test-wttrin-ansi-setup)
(unwind-protect
(let* ((location "Tokyo")
(now 1000.0)
(wttrin-mode-line-favorite-location location)
(mode-line-fetch-count 0)
(main-fetch-count 0))
(cl-letf (((symbol-function 'float-time)
(lambda () now))
((symbol-function 'url-retrieve)
(lambda (url callback)
;; Mode-line uses url-retrieve directly
(setq mode-line-fetch-count (1+ mode-line-fetch-count))
;; Simulate async callback with plain-text format
(with-temp-buffer
(insert "HTTP/1.1 200 OK\n\n")
(insert "Tokyo: ☀️ +25°C Clear") ; Plain text, no ANSI
(funcall callback nil))))
((symbol-function 'wttrin-fetch-raw-string)
(lambda (_location callback)
;; Main buffer fetch should get ANSI codes
(setq main-fetch-count (1+ main-fetch-count))
(funcall callback test-wttrin-ansi-sample-with-colors)))
((symbol-function 'wttrin--cleanup-cache-if-needed)
(lambda () nil)))
;; Step 1: Mode-line fetches weather (simulated)
(wttrin--mode-line-fetch-weather)
;; Mode-line should have fetched
(should (= mode-line-fetch-count 1))
;; Step 2: User clicks mode-line icon (calls wttrin)
(wttrin location)
;; Main fetch should have happened (cache miss)
(should (= main-fetch-count 1))
;; Step 3: Verify buffer has colors
(should (get-buffer "*wttr.in*"))
(with-current-buffer "*wttr.in*"
(let* ((counts (test-wttrin-ansi--count-colored-text))
(colored (car counts))
(total (cdr counts)))
;; Should have colored text
(should (> colored 0))
;; Should be substantial (>10%)
(should (> colored (/ total 10)))))))
(test-wttrin-ansi-teardown)))
(ert-deftest test-wttrin-ansi-cache-stores-ansi-codes ()
"Test that cache stores raw ANSI codes, not filtered text.
This verifies the cache workflow preserves ANSI codes for later filtering."
(test-wttrin-ansi-setup)
(unwind-protect
(let* ((location "Vienna")
(cache-key (wttrin--make-cache-key location))
(now 1000.0)
(fetch-count 0))
(cl-letf (((symbol-function 'float-time)
(lambda () now))
((symbol-function 'wttrin-fetch-raw-string)
(lambda (_location callback)
(setq fetch-count (1+ fetch-count))
;; Simulate fetch returning ANSI codes
(funcall callback test-wttrin-ansi-sample-with-colors)))
((symbol-function 'wttrin--cleanup-cache-if-needed)
(lambda () nil)))
;; Fetch weather (will cache the result)
(wttrin location)
;; Verify fetch was called
(should (= fetch-count 1))
;; Check what's in the cache
(let* ((cached (gethash cache-key wttrin--cache))
(cached-data (cdr cached)))
;; Cache should have data
(should cached)
(should cached-data)
;; CRITICAL: Cache should store RAW ANSI codes
;; NOT filtered text with face properties
(should (stringp cached-data))
(should (string-match-p "\x1b\\[" cached-data))
;; Verify it's the original ANSI string
(should (string-match-p "Partly cloudy" cached-data)))))
(test-wttrin-ansi-teardown)))
(ert-deftest test-wttrin-ansi-fetch-returns-ansi-codes ()
"Test that wttrin-fetch-raw-string returns data with ANSI codes.
This verifies the fetch function returns unfiltered data from wttr.in."
(test-wttrin-ansi-setup)
(unwind-protect
(let ((location "Prague")
(callback-data nil)
(url-retrieve-called nil)
(wttrin-unit-system "u")) ; Set unit system for URL generation
;; Mock url-retrieve to simulate wttr.in response
(cl-letf (((symbol-function 'url-retrieve)
(lambda (url callback)
(setq url-retrieve-called t)
;; Verify URL has ANSI format flag
;; wttr.in uses ?mA, ?uA, or ?A for ANSI colored output
(should (or (string-match-p "\\?mA" url)
(string-match-p "\\?uA" url)
(string-match-p "\\?A" url)))
;; Simulate HTTP response with ANSI codes
(with-temp-buffer
(insert "HTTP/1.1 200 OK\n\n")
(insert test-wttrin-ansi-sample-with-colors)
(funcall callback nil)))))
;; Call fetch
(wttrin-fetch-raw-string
location
(lambda (data)
(setq callback-data data)))
;; Verify url-retrieve was called
(should url-retrieve-called)
;; Verify callback received ANSI codes
(should callback-data)
(should (string-match-p "\x1b\\[" callback-data))))
(test-wttrin-ansi-teardown)))
(ert-deftest test-wttrin-ansi-build-url-includes-ansi-flag ()
"Test that wttrin--build-url includes ANSI color flag in URL.
The 'A' flag tells wttr.in to include ANSI color codes in the response."
(test-wttrin-ansi-setup)
(unwind-protect
(progn
;; Test with metric system
(let ((wttrin-unit-system "m"))
(let ((url (wttrin--build-url "Berlin")))
;; Should have ?mA flag (metric + ANSI)
(should (string-match-p "\\?mA" url))))
;; Test with USCS system
(let ((wttrin-unit-system "u"))
(let ((url (wttrin--build-url "London")))
;; Should have ?uA flag (USCS + ANSI)
(should (string-match-p "\\?uA" url))))
;; Test with no unit system
(let ((wttrin-unit-system nil))
(let ((url (wttrin--build-url "Paris")))
;; Should have just ?A flag (ANSI)
(should (string-match-p "\\?A" url)))))
(test-wttrin-ansi-teardown)))
(provide 'test-wttrin-ansi-color-rendering)
;;; test-wttrin-ansi-color-rendering.el ends here
|