11using Microsoft . UI . Xaml ;
2+ using Microsoft . UI . Xaml . Automation ;
3+ using Microsoft . UI . Xaml . Automation . Peers ;
24using Microsoft . UI . Xaml . Controls ;
5+ using Microsoft . UI . Xaml . Documents ;
36using Microsoft . UI . Xaml . Media ;
47using Microsoft . UI . Xaml . Media . Imaging ;
58using OpenClawTray . Services ;
@@ -50,21 +53,38 @@ private static async Task CaptureWhenSignaledAsync(
5053 if ( ! File . Exists ( signalPath ) )
5154 continue ;
5255
53- await CaptureAsync ( root , surfaceName ) ;
56+ var rootDir = GetVisualTestDirectory ( ) ;
57+ if ( rootDir is null )
58+ return ;
59+ await CaptureToDirectoryAsync (
60+ root ,
61+ Path . Combine ( rootDir , SanitizePathSegment ( surfaceName ) ) ,
62+ Environment . GetEnvironmentVariable (
63+ "OPENCLAW_VISUAL_TEST_ELEMENT_AUTOMATION_ID_PREFIX" ) ,
64+ Environment . GetEnvironmentVariable (
65+ "OPENCLAW_VISUAL_TEST_TEXT" ) ) ;
5466 return ;
5567 }
5668
5769 Logger . Warn ( $ "[VisualTest] Timed out waiting for capture signal for { surfaceName } .") ;
5870 }
5971
60- private static async Task CaptureToDirectoryAsync ( FrameworkElement root , string surfaceDir )
72+ private static async Task CaptureToDirectoryAsync (
73+ FrameworkElement root ,
74+ string surfaceDir ,
75+ string ? automationIdPrefix = null ,
76+ string ? exactText = null )
6177 {
6278 try
6379 {
6480 Directory . CreateDirectory ( surfaceDir ) ;
6581 if ( root . DispatcherQueue . HasThreadAccess )
6682 {
67- await CaptureOnUiThreadAsync ( root , surfaceDir ) ;
83+ foreach ( var captureRoot in ResolveCaptureRoots (
84+ root ,
85+ automationIdPrefix ,
86+ exactText ) )
87+ await CaptureOnUiThreadAsync ( captureRoot , surfaceDir ) ;
6888 return ;
6989 }
7090
@@ -73,7 +93,11 @@ private static async Task CaptureToDirectoryAsync(FrameworkElement root, string
7393 {
7494 try
7595 {
76- await CaptureOnUiThreadAsync ( root , surfaceDir ) ;
96+ foreach ( var captureRoot in ResolveCaptureRoots (
97+ root ,
98+ automationIdPrefix ,
99+ exactText ) )
100+ await CaptureOnUiThreadAsync ( captureRoot , surfaceDir ) ;
77101 tcs . SetResult ( ) ;
78102 }
79103 catch ( Exception ex )
@@ -93,6 +117,96 @@ private static async Task CaptureToDirectoryAsync(FrameworkElement root, string
93117 }
94118 }
95119
120+ private static IReadOnlyList < FrameworkElement > ResolveCaptureRoots (
121+ FrameworkElement root ,
122+ string ? automationIdPrefix ,
123+ string ? exactText )
124+ {
125+ if ( string . IsNullOrWhiteSpace ( automationIdPrefix )
126+ && string . IsNullOrWhiteSpace ( exactText ) )
127+ return [ root ] ;
128+
129+ var matches = new List < FrameworkElement > ( ) ;
130+ FindDescendants ( root , automationIdPrefix , exactText , matches ) ;
131+ if ( matches . Count == 0 )
132+ {
133+ throw new InvalidOperationException (
134+ "Could not find the requested visual elements for capture." ) ;
135+ }
136+
137+ return matches ;
138+ }
139+
140+ private static void FindDescendants (
141+ FrameworkElement root ,
142+ string ? automationIdPrefix ,
143+ string ? exactText ,
144+ ICollection < FrameworkElement > matches )
145+ {
146+ if ( ! string . IsNullOrWhiteSpace ( automationIdPrefix )
147+ && AutomationProperties . GetAutomationId ( root ) . StartsWith (
148+ automationIdPrefix ,
149+ StringComparison . Ordinal ) )
150+ {
151+ matches . Add ( root ) ;
152+ }
153+ if ( ! string . IsNullOrWhiteSpace ( exactText )
154+ && MatchesText ( root , exactText ) )
155+ {
156+ matches . Add ( FindScrollViewer ( root ) ?? root ) ;
157+ }
158+
159+ var childCount = VisualTreeHelper . GetChildrenCount ( root ) ;
160+ for ( var index = 0 ; index < childCount ; index ++ )
161+ {
162+ if ( VisualTreeHelper . GetChild ( root , index ) is FrameworkElement child )
163+ FindDescendants ( child , automationIdPrefix , exactText , matches ) ;
164+ }
165+ }
166+
167+ private static FrameworkElement ? FindScrollViewer ( FrameworkElement element )
168+ {
169+ var current = VisualTreeHelper . GetParent ( element ) ;
170+ for ( var depth = 0 ; current is not null && depth < 16 ; depth ++ )
171+ {
172+ if ( current is ScrollViewer scrollViewer )
173+ return scrollViewer ;
174+ current = VisualTreeHelper . GetParent ( current ) ;
175+ }
176+
177+ return null ;
178+ }
179+
180+ private static bool MatchesText ( FrameworkElement element , string exactText )
181+ {
182+ var text = ReadText ( element ) ? . TrimEnd ( '\r ' , '\n ' ) ;
183+ if ( string . Equals ( text , exactText , StringComparison . Ordinal ) )
184+ return true ;
185+
186+ var peer = FrameworkElementAutomationPeer . FromElement ( element )
187+ ?? FrameworkElementAutomationPeer . CreatePeerForElement ( element ) ;
188+ return string . Equals ( peer ? . GetName ( ) , exactText , StringComparison . Ordinal ) ;
189+ }
190+
191+ private static string ? ReadText ( FrameworkElement element ) => element switch
192+ {
193+ TextBlock textBlock => textBlock . Text ,
194+ RichTextBlock richTextBlock => string . Concat (
195+ richTextBlock . Blocks
196+ . OfType < Paragraph > ( )
197+ . SelectMany ( paragraph => paragraph . Inlines )
198+ . Select ( ReadInline ) ) ,
199+ _ => null ,
200+ } ;
201+
202+ private static string ReadInline ( Inline inline ) => inline switch
203+ {
204+ Run run => run . Text ,
205+ Span span => string . Concat ( span . Inlines . Select ( ReadInline ) ) ,
206+ LineBreak => Environment . NewLine ,
207+ _ => string . Empty ,
208+ } ;
209+
96210 private static async Task CaptureOnUiThreadAsync ( FrameworkElement root , string surfaceDir )
97211 {
98212 Action restoreBackground = ( ) => { } ;
0 commit comments