@@ -14,9 +14,62 @@ import {
1414 StatusBar ,
1515 InputBox ,
1616 EditorView ,
17- SettingsEditor
1817} from "vscode-extension-tester" ;
1918
19+ // ---------------------------------------------------------------------------
20+ // Polling helpers (replace fixed setTimeout waits)
21+ // ---------------------------------------------------------------------------
22+
23+ /** Poll fn until it returns a defined value, or throw after timeoutMs. */
24+ async function poll < T > (
25+ fn : ( ) => Promise < T | undefined > ,
26+ timeoutMs = 5000 ,
27+ ) : Promise < T > {
28+ const deadline = Date . now ( ) + timeoutMs ;
29+ while ( Date . now ( ) < deadline ) {
30+ const result = await fn ( ) ;
31+ if ( result !== undefined ) return result ;
32+ await new Promise ( ( r ) => setTimeout ( r , 250 ) ) ;
33+ }
34+ throw new Error ( `poll timed out after ${ timeoutMs } ms` ) ;
35+ }
36+
37+ /** Find and return the text of a status bar item containing "Patchloom". */
38+ async function findPatchloomStatus ( ) : Promise < string | undefined > {
39+ for ( const item of await new StatusBar ( ) . getItems ( ) ) {
40+ try {
41+ const text = await item . getText ( ) ;
42+ if ( text . includes ( "Patchloom" ) ) return text ;
43+ } catch {
44+ // some items may not expose accessible text
45+ }
46+ }
47+ return undefined ;
48+ }
49+
50+ /** Find, dismiss, and return a notification matching the given pattern. */
51+ async function findNotification (
52+ workbench : Workbench ,
53+ pattern : RegExp ,
54+ ) : Promise < string | undefined > {
55+ for ( const n of await workbench . getNotifications ( ) ) {
56+ try {
57+ const msg = await n . getMessage ( ) ;
58+ if ( pattern . test ( msg ) ) {
59+ await n . dismiss ( ) ;
60+ return msg ;
61+ }
62+ } catch {
63+ // notification may have auto-dismissed
64+ }
65+ }
66+ return undefined ;
67+ }
68+
69+ // ---------------------------------------------------------------------------
70+ // Tests
71+ // ---------------------------------------------------------------------------
72+
2073describe ( "Patchloom Extension UI" , function ( ) {
2174 this . timeout ( 60_000 ) ;
2275
@@ -26,236 +79,113 @@ describe("Patchloom Extension UI", function () {
2679 this . timeout ( 90_000 ) ;
2780 await VSBrowser . instance . waitForWorkbench ( ) ;
2881 workbench = new Workbench ( ) ;
29- // Give the extension time to activate (onStartupFinished)
30- await new Promise ( ( resolve ) => setTimeout ( resolve , 3000 ) ) ;
82+ // Wait for the extension to activate and create its status bar item
83+ await poll ( findPatchloomStatus , 15_000 ) ;
3184 } ) ;
3285
3386 describe ( "Status bar" , function ( ) {
3487 it ( "should display a Patchloom status bar item" , async function ( ) {
35- const statusBar = new StatusBar ( ) ;
36- // The extension creates a status bar item with text containing "Patchloom"
37- const items = await statusBar . getItems ( ) ;
38- const patchloomItem = items . find (
39- ( item ) => item !== undefined
40- ) ;
41- // At minimum, the status bar should have items (VS Code always has some)
42- assert . ok ( items . length > 0 , "status bar should have items" ) ;
43-
44- // Try to find our specific item by partial text match
45- let found = false ;
46- for ( const item of items ) {
47- try {
48- const text = await item . getText ( ) ;
49- if ( text . includes ( "Patchloom" ) ) {
50- found = true ;
51- break ;
52- }
53- } catch {
54- // Some items may not have accessible text
55- }
56- }
57- assert . ok ( found , "status bar should contain a Patchloom item" ) ;
88+ const text = await poll ( findPatchloomStatus ) ;
89+ assert . ok ( text . includes ( "Patchloom" ) , "status bar should contain a Patchloom item" ) ;
5890 } ) ;
5991
60- it ( "should show warning icon when patchloom binary is not found" , async function ( ) {
61- const statusBar = new StatusBar ( ) ;
62- const items = await statusBar . getItems ( ) ;
63- let text = "" ;
64- for ( const item of items ) {
65- try {
66- const t = await item . getText ( ) ;
67- if ( t . includes ( "Patchloom" ) ) {
68- text = t ;
69- break ;
70- }
71- } catch {
72- // skip
73- }
74- }
75- // In the test environment, patchloom is likely not on PATH,
76- // so the status bar should show the warning variant
77- // (unless patchloom.path is configured, which it shouldn't be in test)
92+ it ( "should have non-empty status text" , async function ( ) {
93+ const text = await poll ( findPatchloomStatus ) ;
7894 assert . ok ( text . length > 0 , "Patchloom status text should not be empty" ) ;
7995 } ) ;
8096 } ) ;
8197
8298 describe ( "Show Status command" , function ( ) {
83- it ( "should display a status message when invoked" , async function ( ) {
99+ it ( "should display a notification when invoked" , async function ( ) {
84100 await workbench . executeCommand ( "Patchloom: Show Status" ) ;
85- // The command shows an information or warning message
86- // Wait for notification to appear
87- await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
88-
89- const notifications = await workbench . getNotifications ( ) ;
90- let foundPatchloom = false ;
91- for ( const notification of notifications ) {
92- try {
93- const msg = await notification . getMessage ( ) ;
94- if ( msg . includes ( "Patchloom" ) || msg . includes ( "patchloom" ) ) {
95- foundPatchloom = true ;
96- // Dismiss the notification
97- await notification . dismiss ( ) ;
98- break ;
99- }
100- } catch {
101- // notification may have auto-dismissed
102- }
103- }
104- assert . ok ( foundPatchloom , "Show Status should display a notification mentioning Patchloom" ) ;
101+ const msg = await poll (
102+ ( ) => findNotification ( workbench , / [ P p ] a t c h l o o m / ) ,
103+ ) ;
104+ assert . ok ( msg , "Show Status should display a notification mentioning Patchloom" ) ;
105105 } ) ;
106106 } ) ;
107107
108108 describe ( "Open Settings command" , function ( ) {
109109 it ( "should open settings filtered to patchloom" , async function ( ) {
110110 await workbench . executeCommand ( "Patchloom: Open Settings" ) ;
111- await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
112-
113- // Verify the settings editor opened
114- const editorView = new EditorView ( ) ;
115- const titles = await editorView . getOpenEditorTitles ( ) ;
116- const hasSettings = titles . some (
117- ( title ) => title . toLowerCase ( ) . includes ( "settings" )
118- ) ;
119- assert . ok ( hasSettings , "Settings editor should be open after Open Settings command" ) ;
120-
121- // Close the settings tab
122- await editorView . closeAllEditors ( ) ;
111+ await poll ( async ( ) => {
112+ const titles = await new EditorView ( ) . getOpenEditorTitles ( ) ;
113+ return titles . some ( ( t ) => t . toLowerCase ( ) . includes ( "settings" ) ) || undefined ;
114+ } ) ;
115+ await new EditorView ( ) . closeAllEditors ( ) ;
123116 } ) ;
124117 } ) ;
125118
126119 describe ( "Configure MCP command" , function ( ) {
127- it ( "should show a quick pick or warning when invoked" , async function ( ) {
120+ it ( "should show a notification or quick pick when invoked" , async function ( ) {
128121 await workbench . executeCommand ( "Patchloom: Configure MCP" ) ;
129- await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
130122
131- // If patchloom is not found, it shows a warning notification.
132- // If patchloom is found, it shows a quick pick for target selection.
133- // Either way, something should appear.
134- const notifications = await workbench . getNotifications ( ) ;
135- let gotNotification = false ;
136- for ( const notification of notifications ) {
137- try {
138- const msg = await notification . getMessage ( ) ;
139- if ( msg . includes ( "Patchloom" ) || msg . includes ( "patchloom" ) || msg . includes ( "MCP" ) ) {
140- gotNotification = true ;
141- await notification . dismiss ( ) ;
142- break ;
143- }
144- } catch {
145- // skip
146- }
147- }
148-
149- if ( ! gotNotification ) {
150- // A quick pick may have appeared instead (patchloom is installed)
123+ // The command shows either a warning notification (binary not found)
124+ // or a quick pick for target selection (binary found).
125+ const result = await poll < { kind : string } > ( async ( ) => {
126+ const msg = await findNotification ( workbench , / [ P p ] a t c h l o o m | M C P / ) ;
127+ if ( msg ) return { kind : "notification" } ;
151128 try {
152129 const input = new InputBox ( ) ;
153- await input . cancel ( ) ;
154- gotNotification = true ;
130+ await input . getText ( ) ; // throws if no input box visible
131+ return { kind : "input" } ;
155132 } catch {
156- // No input box either; that's also acceptable if command completed silently
157- gotNotification = true ;
133+ return undefined ; // neither appeared yet, keep polling
158134 }
159- }
135+ } ) ;
160136
161- assert . ok ( gotNotification , "Configure MCP should show a notification or quick pick" ) ;
137+ assert . ok ( result , "Configure MCP should produce a notification or quick pick" ) ;
138+ if ( result . kind === "input" ) {
139+ try { await new InputBox ( ) . cancel ( ) ; } catch { /* already gone */ }
140+ }
162141 } ) ;
163142 } ) ;
164143
165144 describe ( "Quick Action command" , function ( ) {
166- it ( "should show a quick pick or warning when invoked" , async function ( ) {
145+ it ( "should show a notification or quick pick when invoked" , async function ( ) {
167146 await workbench . executeCommand ( "Patchloom: Quick Action" ) ;
168- await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
169147
170- const notifications = await workbench . getNotifications ( ) ;
171- let gotNotification = false ;
172- for ( const notification of notifications ) {
173- try {
174- const msg = await notification . getMessage ( ) ;
175- if ( msg . includes ( "Patchloom" ) || msg . includes ( "patchloom" ) ) {
176- gotNotification = true ;
177- await notification . dismiss ( ) ;
178- break ;
179- }
180- } catch {
181- // skip
182- }
183- }
184-
185- if ( ! gotNotification ) {
148+ const result = await poll < { kind : string ; labels ?: string [ ] } > ( async ( ) => {
149+ const msg = await findNotification ( workbench , / [ P p ] a t c h l o o m / ) ;
150+ if ( msg ) return { kind : "notification" } ;
186151 try {
187152 const input = new InputBox ( ) ;
188- // If the quick pick appeared, verify it has our options
189153 const picks = await input . getQuickPicks ( ) ;
190- if ( picks . length > 0 ) {
191- const labels : string [ ] = [ ] ;
192- for ( const pick of picks ) {
193- labels . push ( await pick . getLabel ( ) ) ;
194- }
195- const hasReplace = labels . some ( ( l ) => l . includes ( "Replace" ) ) ;
196- const hasTidy = labels . some ( ( l ) => l . includes ( "Tidy" ) ) ;
197- assert . ok ( hasReplace || hasTidy ,
198- `quick pick should contain Replace or Tidy, got: ${ labels . join ( ", " ) } ` ) ;
199- }
200- await input . cancel ( ) ;
201- gotNotification = true ;
154+ const labels : string [ ] = [ ] ;
155+ for ( const pick of picks ) labels . push ( await pick . getLabel ( ) ) ;
156+ return { kind : "input" , labels } ;
202157 } catch {
203- gotNotification = true ;
158+ return undefined ; // keep polling
204159 }
160+ } ) ;
161+
162+ assert . ok ( result , "Quick Action should produce a notification or quick pick" ) ;
163+ if ( result . kind === "input" ) {
164+ assert . ok (
165+ result . labels ! . some ( ( l ) => l . includes ( "Replace" ) || l . includes ( "Tidy" ) ) ,
166+ `quick pick should contain Replace or Tidy, got: ${ result . labels ! . join ( ", " ) } ` ,
167+ ) ;
168+ try { await new InputBox ( ) . cancel ( ) ; } catch { /* already gone */ }
205169 }
206-
207- assert . ok ( gotNotification , "Quick Action should show a notification or quick pick" ) ;
208170 } ) ;
209171 } ) ;
210172
211173 describe ( "Configuration changes" , function ( ) {
212- it ( "should react to showStatusBar being toggled off and on" , async function ( ) {
213- const statusBar = new StatusBar ( ) ;
214-
215- // Verify Patchloom is in the status bar initially
216- let items = await statusBar . getItems ( ) ;
217- let hasPatchloom = false ;
218- for ( const item of items ) {
219- try {
220- if ( ( await item . getText ( ) ) . includes ( "Patchloom" ) ) {
221- hasPatchloom = true ;
222- break ;
223- }
224- } catch {
225- // skip
226- }
227- }
228- assert . ok ( hasPatchloom , "Patchloom should be in status bar initially" ) ;
229-
230- // Open settings and toggle showStatusBar off
174+ it ( "should show Patchloom setting in the settings editor" , async function ( ) {
175+ await poll ( findPatchloomStatus ) ;
231176 const settingsEditor = await workbench . openSettings ( ) ;
232- await settingsEditor . findSetting ( "Show Status Bar" , "Patchloom" ) ;
233- // We verified it opens; toggling programmatically is more reliable
234- // than clicking the checkbox via WebDriver
177+ const setting = await settingsEditor . findSetting ( "Show Status Bar" , "Patchloom" ) ;
178+ assert . ok ( setting , "should find the showStatusBar setting" ) ;
235179 await workbench . executeCommand ( "workbench.action.closeActiveEditor" ) ;
236180 } ) ;
237181 } ) ;
238182
239183 after ( async function ( ) {
240- // Clean up: close all editors and dismiss notifications
241- try {
242- const editorView = new EditorView ( ) ;
243- await editorView . closeAllEditors ( ) ;
244- } catch {
245- // may not have any editors open
246- }
247-
184+ try { await new EditorView ( ) . closeAllEditors ( ) ; } catch { /* none open */ }
248185 try {
249- const notifications = await workbench . getNotifications ( ) ;
250- for ( const notification of notifications ) {
251- try {
252- await notification . dismiss ( ) ;
253- } catch {
254- // already dismissed
255- }
186+ for ( const n of await workbench . getNotifications ( ) ) {
187+ try { await n . dismiss ( ) ; } catch { /* already dismissed */ }
256188 }
257- } catch {
258- // no notifications
259- }
189+ } catch { /* no notifications */ }
260190 } ) ;
261191} ) ;
0 commit comments