66
77Walks through Settings → Debug Log and verifies:
88 1. The viewer page loads with a visible search bar and log list.
9- 2. The log list is non-empty (entries were actually loaded) .
9+ 2. The initial load is capped while older entries remain available .
1010 3. Typing in the search field filters the list; clearing it restores
1111 the full count.
12+ 4. Auto-refresh replaces the capped tail without growing it unboundedly.
13+ 5. The list virtualizes offscreen rows and can load older rows on demand.
1214
1315This test requires the binary to be built with -DENABLE_TEST_AUTOMATION=ON.
1416"""
2224import tempfile
2325
2426from qml_test_harness import GUI_STARTUP_TIMEOUT , dump_qml_tree , find_gui_binary , setup_datadir
25- from qml_driver import QmlDriver , QmlDriverError
27+ from qml_driver import QmlDriver
28+
29+
30+ INITIAL_LOAD_LIMIT = 1000
31+ SEEDED_HISTORY_LINES = 1200
2632
2733
2834def assert_close (actual , expected , label , tolerance = 1 ):
@@ -43,6 +49,19 @@ def __init__(self):
4349 self .process = None
4450 self .driver = None
4551 self .datadir = setup_datadir (self .tmpdir )
52+ self ._seed_debug_log_history ()
53+
54+ def _seed_debug_log_history (self ):
55+ """Create enough history to exercise the initial cap and Load more."""
56+ network_dir = os .path .join (self .datadir , "regtest" )
57+ os .makedirs (network_dir , exist_ok = True )
58+ log_path = os .path .join (network_dir , "debug.log" )
59+ with open (log_path , "w" , encoding = "utf-8" ) as f :
60+ for i in range (SEEDED_HISTORY_LINES ):
61+ f .write (
62+ "2026-01-01T00:00:00Z "
63+ f"test-automation seeded-history marker { i } \n "
64+ )
4665
4766 def start (self ):
4867 env = dict (os .environ )
@@ -114,10 +133,15 @@ def test_viewer_visibility(gui):
114133
115134
116135def test_log_has_entries (gui ):
117- """Verify that log entries were actually loaded into the list view ."""
136+ """Verify that the first snapshot is capped at 1,000 rows ."""
118137 print ("\n ── test_log_has_entries ──────────────────────────────────────────" )
119- count = gui .get_property ("debugLogListView" , "count" )
120- assert count > 0 , f"Expected log entries to be loaded, got count={ count } "
138+ count = gui .wait_for_property (
139+ "debugLogListView" , "count" , INITIAL_LOAD_LIMIT , timeout_ms = 5000
140+ )
141+ assert count == INITIAL_LOAD_LIMIT , (
142+ f"Expected the initial load to be capped at { INITIAL_LOAD_LIMIT } , "
143+ f"got count={ count } "
144+ )
121145 print (f" PASSED: debugLogListView.count = { count } " )
122146 return count
123147
@@ -130,7 +154,6 @@ def test_search_layout_matches_design(gui):
130154 gui .wait_for_property ("debugLogSearchDivider" , "height" , 1 , timeout_ms = 3000 )
131155 gui .wait_for_property ("debugLogListView" , "topPadding" , 10 , timeout_ms = 3000 )
132156 gui .wait_for_property ("debugLogListView_row_0" , "visible" , True , timeout_ms = 3000 )
133- gui .wait_for_property ("debugLogListView_row_0" , "y" , 10 , timeout_ms = 3000 )
134157 gui .wait_for_property ("debugLogListView_lineNumber_0" , "visible" , True , timeout_ms = 3000 )
135158 gui .wait_for_property ("debugLogListView_date_0" , "visible" , True , timeout_ms = 3000 )
136159
@@ -147,6 +170,13 @@ def test_search_layout_matches_design(gui):
147170 divider_y = gui .get_property ("debugLogSearchDivider" , "y" )
148171 divider_height = gui .get_property ("debugLogSearchDivider" , "height" )
149172 list_y = gui .get_property ("debugLogListView" , "y" )
173+ first_row_viewport_y = (
174+ gui .get_property ("debugLogListView_row_0" , "y" )
175+ - gui .get_property ("debugLogListView" , "contentY" )
176+ )
177+ effective_line_number_width = gui .get_property (
178+ "debugLogListView" , "effectiveLineNumberWidth"
179+ )
150180 page_width = gui .get_property ("settingsDebugLog" , "width" )
151181 content_width = gui .get_property ("debugLogContentLayout" , "width" )
152182 expected_content_width = max (0 , min (page_width - 40 , 600 ))
@@ -197,20 +227,24 @@ def test_search_layout_matches_design(gui):
197227 "debug log entry command/content spacing" )
198228 assert_close (gui .get_property ("debugLogListView" , "lineNumberWidth" ), 20 ,
199229 "debug log line number slot width" )
200- assert_close (gui .get_property ("debugLogListView" , "effectiveLineNumberWidth" ), 20 ,
201- "debug log effective line number slot width" )
230+ assert effective_line_number_width >= 20 , (
231+ "Expected the effective line-number slot to honor its 20px minimum"
232+ )
202233 assert_close (gui .get_property ("debugLogListView" , "fontPixelSize" ), 12 ,
203234 "debug log entry font size" )
204235 assert_close (gui .get_property ("debugLogListView" , "textLineHeight" ), 17 ,
205236 "debug log entry line height" )
206- assert_close (gui . get_property ( "debugLogListView_row_0" , "y" ) , 10 ,
207- "first debug log row y" )
237+ assert_close (first_row_viewport_y , 10 ,
238+ "first debug log row viewport y" )
208239 assert_close (gui .get_property ("debugLogListView_row_0" , "spacing" ), 10 ,
209240 "first debug log row column gap" )
210241 assert_close (gui .get_property ("debugLogListView_entryContent_0" , "spacing" ), 2 ,
211242 "first debug log entry internal gap" )
212- assert_close (gui .get_property ("debugLogListView_lineNumber_0" , "width" ), 20 ,
213- "first debug log line number width" )
243+ assert_close (
244+ gui .get_property ("debugLogListView_lineNumber_0" , "width" ),
245+ effective_line_number_width ,
246+ "first debug log line number width" ,
247+ )
214248 first_text_object = (
215249 "debugLogListView_message_0"
216250 if first_row_has_command
@@ -236,46 +270,56 @@ def test_refresh_button(gui, original_count):
236270
237271
238272def test_auto_refresh (gui , datadir , current_count ):
239- """Appending to debug.log triggers auto-refresh and grows the entry count ."""
273+ """Appending refreshes the capped tail without exceeding its load limit ."""
240274 print ("\n ── test_auto_refresh ─────────────────────────────────────────────" )
241275 log_path = os .path .join (datadir , "regtest" , "debug.log" )
242276 ts = datetime .datetime .now (datetime .timezone .utc ).strftime ("%Y-%m-%dT%H:%M:%SZ" )
243277 marker_body = "test-automation auto-refresh marker"
244278 marker = f"{ ts } { marker_body } "
245279 with open (log_path , "a" , encoding = "utf-8" ) as f :
246280 f .write (marker + "\n " )
247- try :
248- new_count = gui .wait_for_property (
249- "debugLogListView" , "count" , lambda c : c > current_count , timeout_ms = 5000
250- )
251- except QmlDriverError :
252- gui .click ("debugLogRefreshButton" )
253- new_count = gui .wait_for_property (
254- "debugLogListView" , "count" , lambda c : c > current_count , timeout_ms = 3000
255- )
256- assert new_count > current_count , (
257- f"Expected count to grow beyond { current_count } after appending to debug.log, "
258- f"got { new_count } "
259- )
260- print (f" PASSED: count grew from { current_count } to { new_count } after file append" )
261281
282+ # A capped model can insert the new row and remove one old row without
283+ # changing count. Filter for the unique marker to observe the actual data
284+ # update rather than treating row-count growth as the refresh signal.
262285 gui .set_text ("debugLogSearchField" , marker_body )
263- gui .wait_for_property ("debugLogListView" , "count" , lambda c : c >= 1 , timeout_ms = 3000 )
286+ gui .wait_for_property ("debugLogListView" , "count" , 1 , timeout_ms = 5000 )
287+ gui .wait_for_property (
288+ "debugLogListView_commandlessMessage_0" ,
289+ "text" ,
290+ marker_body ,
291+ timeout_ms = 3000 ,
292+ )
293+
264294 gui .wait_for_property ("debugLogListView_command_0" , "visible" , False , timeout_ms = 3000 )
265295 gui .wait_for_property ("debugLogListView_commandlessMessage_0" , "visible" , True , timeout_ms = 3000 )
266- gui .wait_for_property ("debugLogListView_commandlessMessage_0" , "text" , marker_body , timeout_ms = 3000 )
267296 gui .wait_for_property ("debugLogListView_message_0" , "visible" , False , timeout_ms = 3000 )
297+ gui .invoke ("debugLogListView_commandlessMessage_0" , "selectAll" )
298+ gui .wait_for_property (
299+ "debugLogListView_commandlessMessage_0" ,
300+ "selectedText" ,
301+ marker_body ,
302+ timeout_ms = 3000 ,
303+ )
268304 print (" PASSED: commandless log entry renders inline with the date" )
269305
270306 gui .set_text ("debugLogSearchField" , "" )
271307 restored_count = gui .wait_for_property (
272- "debugLogListView" , "count" , lambda c : c >= new_count , timeout_ms = 3000
308+ "debugLogListView" , "count" , current_count , timeout_ms = 3000
309+ )
310+ assert restored_count == INITIAL_LOAD_LIMIT , (
311+ f"Expected auto-refresh to retain the { INITIAL_LOAD_LIMIT } -row cap, "
312+ f"got { restored_count } "
313+ )
314+ print (
315+ f" PASSED: appended entry loaded and count remained capped at "
316+ f"{ restored_count } "
273317 )
274318 return restored_count
275319
276320
277321def test_four_digit_line_numbers_are_not_clipped (gui , datadir , current_count ):
278- """Verify the line-number column expands once visible numbers reach four digits ."""
322+ """Verify virtualized offscreen rows and the four-digit number column ."""
279323 print ("\n ── test_four_digit_line_numbers_are_not_clipped ────────────────" )
280324 target_count = 1000
281325 needed = max (0 , target_count - current_count )
@@ -293,20 +337,63 @@ def test_four_digit_line_numbers_are_not_clipped(gui, datadir, current_count):
293337 f"Expected at least { target_count } lines after append, got { count } "
294338 )
295339
296- gui . wait_for_property (
297- "debugLogListView_lineNumber_999" , "text" , str ( target_count ), timeout_ms = 3000
298- )
299- line_number_width = gui .get_property ( "debugLogListView_lineNumber_999 " , "width" )
300- line_number_implicit_width = gui .get_property (
301- "debugLogListView_lineNumber_999" , "implicitWidth "
340+ last_index = count - 1
341+ last_line_number = f"debugLogListView_lineNumber_ { last_index } "
342+ gui . invoke ( "debugLogListView" , "scrollToTop" )
343+ gui .wait_for_property ( "debugLogListView " , "atTop" , True , timeout_ms = 3000 )
344+ assert not gui .object_exists ( last_line_number ), (
345+ f"Expected offscreen row { last_index } not to be instantiated at the top "
302346 )
347+
348+ gui .invoke ("debugLogListView" , "scrollToBottom" )
349+ gui .wait_for_property ("debugLogListView" , "atBottom" , True , timeout_ms = 3000 )
350+ gui .wait_for_property (last_line_number , "text" , str (count ), timeout_ms = 3000 )
351+ line_number_width = gui .get_property (last_line_number , "width" )
352+ line_number_implicit_width = gui .get_property (last_line_number , "implicitWidth" )
303353 assert line_number_width >= line_number_implicit_width , (
304354 f"Expected four-digit line number width { line_number_width } to fit "
305355 f"implicit width { line_number_implicit_width } "
306356 )
307357 assert gui .get_property ("debugLogListView" , "effectiveLineNumberWidth" ) > 20 , \
308358 "Expected effective line-number slot to expand beyond the design minimum"
309- print (" PASSED: four-digit line numbers fit expanded line-number slot" )
359+ print (" PASSED: offscreen rows are virtualized and four-digit line numbers fit" )
360+
361+
362+ def test_load_more_at_bottom (gui , current_count ):
363+ """The bottom affordance appends older rows without moving the viewport."""
364+ print ("\n ── test_load_more_at_bottom ───────────────────────" )
365+ gui .invoke ("debugLogListView" , "scrollToBottom" )
366+ gui .wait_for_property ("debugLogListView" , "atBottom" , True , timeout_ms = 3000 )
367+ gui .wait_for_property ("debugLogLoadMoreButton" , "visible" , True , timeout_ms = 3000 )
368+ content_y_before = gui .get_property ("debugLogListView" , "contentY" )
369+
370+ gui .click ("debugLogLoadMoreButton" )
371+ expanded_count = gui .wait_for_property (
372+ "debugLogListView" , "count" , lambda c : c > current_count , timeout_ms = 5000
373+ )
374+ content_y_after = gui .get_property ("debugLogListView" , "contentY" )
375+ assert_close (
376+ content_y_after ,
377+ content_y_before ,
378+ "load-more viewport anchor" ,
379+ tolerance = 2 ,
380+ )
381+ gui .wait_for_property ("debugLogLoadMoreButton" , "visible" , False , timeout_ms = 3000 )
382+
383+ oldest_index = expanded_count - 1
384+ oldest_message = f"debugLogListView_commandlessMessage_{ oldest_index } "
385+ gui .invoke ("debugLogListView" , "scrollToBottom" )
386+ gui .wait_for_property (
387+ oldest_message ,
388+ "text" ,
389+ "test-automation seeded-history marker 0" ,
390+ timeout_ms = 3000 ,
391+ )
392+ print (
393+ f" PASSED: loaded { expanded_count - current_count } older rows without "
394+ "moving the viewport, including the oldest seeded row"
395+ )
396+ return expanded_count
310397
311398
312399def test_close_settings (gui ):
@@ -363,9 +450,10 @@ def run_tests():
363450 total = test_log_has_entries (gui )
364451 test_search_layout_matches_design (gui )
365452 test_search_filter (gui , total )
366- total = test_refresh_button (gui , total )
367453 total = test_auto_refresh (gui , harness .datadir , total )
454+ total = test_refresh_button (gui , total )
368455 test_four_digit_line_numbers_are_not_clipped (gui , harness .datadir , total )
456+ total = test_load_more_at_bottom (gui , total )
369457 test_close_settings (gui )
370458
371459 print ("\n " + "=" * 50 )
0 commit comments