11import { test , expect } from "../fixtures"
2- import { cleanupSession , clearSessionDockSeed , seedSessionQuestion , seedSessionTodos } from "../actions"
2+ import { composerEvent , type ComposerDriverState , type ComposerProbeState , type ComposerWindow } from "../../src/testing/session-composer"
3+ import { cleanupSession , clearSessionDockSeed , seedSessionQuestion } from "../actions"
34import {
45 permissionDockSelector ,
56 promptSelector ,
67 questionDockSelector ,
78 sessionComposerDockSelector ,
8- sessionTodoDockSelector ,
9- sessionTodoListSelector ,
109 sessionTodoToggleButtonSelector ,
1110} from "../selectors"
1211
@@ -42,12 +41,8 @@ async function withDockSeed<T>(sdk: Sdk, sessionID: string, fn: () => Promise<T>
4241
4342async function clearPermissionDock ( page : any , label : RegExp ) {
4443 const dock = page . locator ( permissionDockSelector )
45- for ( let i = 0 ; i < 3 ; i ++ ) {
46- const count = await dock . count ( )
47- if ( count === 0 ) return
48- await dock . getByRole ( "button" , { name : label } ) . click ( )
49- await page . waitForTimeout ( 150 )
50- }
44+ await expect ( dock ) . toBeVisible ( )
45+ await dock . getByRole ( "button" , { name : label } ) . click ( )
5146}
5247
5348async function setAutoAccept ( page : any , enabled : boolean ) {
@@ -59,6 +54,120 @@ async function setAutoAccept(page: any, enabled: boolean) {
5954 await expect ( button ) . toHaveAttribute ( "aria-pressed" , enabled ? "true" : "false" )
6055}
6156
57+ async function expectQuestionBlocked ( page : any ) {
58+ await expect ( page . locator ( questionDockSelector ) ) . toBeVisible ( )
59+ await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
60+ }
61+
62+ async function expectQuestionOpen ( page : any ) {
63+ await expect ( page . locator ( questionDockSelector ) ) . toHaveCount ( 0 )
64+ await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
65+ }
66+
67+ async function expectPermissionBlocked ( page : any ) {
68+ await expect ( page . locator ( permissionDockSelector ) ) . toBeVisible ( )
69+ await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
70+ }
71+
72+ async function expectPermissionOpen ( page : any ) {
73+ await expect ( page . locator ( permissionDockSelector ) ) . toHaveCount ( 0 )
74+ await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
75+ }
76+
77+ async function todoDock ( page : any , sessionID : string ) {
78+ await page . addInitScript ( ( ) => {
79+ const win = window as ComposerWindow
80+ win . __opencode_e2e = {
81+ ...win . __opencode_e2e ,
82+ composer : {
83+ enabled : true ,
84+ sessions : { } ,
85+ } ,
86+ }
87+ } )
88+
89+ const write = async ( driver : ComposerDriverState | undefined ) => {
90+ await page . evaluate (
91+ ( input ) => {
92+ const win = window as ComposerWindow
93+ const composer = win . __opencode_e2e ?. composer
94+ if ( ! composer ?. enabled ) throw new Error ( "Composer e2e driver is not enabled" )
95+ composer . sessions ??= { }
96+ const prev = composer . sessions [ input . sessionID ] ?? { }
97+ if ( ! input . driver ) {
98+ if ( ! prev . probe ) {
99+ delete composer . sessions [ input . sessionID ]
100+ } else {
101+ composer . sessions [ input . sessionID ] = { probe : prev . probe }
102+ }
103+ } else {
104+ composer . sessions [ input . sessionID ] = {
105+ ...prev ,
106+ driver : input . driver ,
107+ }
108+ }
109+ window . dispatchEvent ( new CustomEvent ( input . event , { detail : { sessionID : input . sessionID } } ) )
110+ } ,
111+ { event : composerEvent , sessionID, driver } ,
112+ )
113+ }
114+
115+ const read = ( ) =>
116+ page . evaluate ( ( sessionID ) => {
117+ const win = window as ComposerWindow
118+ return win . __opencode_e2e ?. composer ?. sessions ?. [ sessionID ] ?. probe ?? null
119+ } , sessionID ) as Promise < ComposerProbeState | null >
120+
121+ const api = {
122+ async clear ( ) {
123+ await write ( undefined )
124+ return api
125+ } ,
126+ async open ( todos : NonNullable < ComposerDriverState [ "todos" ] > ) {
127+ await write ( { live : true , todos } )
128+ return api
129+ } ,
130+ async finish ( todos : NonNullable < ComposerDriverState [ "todos" ] > ) {
131+ await write ( { live : false , todos } )
132+ return api
133+ } ,
134+ async expectOpen ( states : ComposerProbeState [ "states" ] ) {
135+ await expect . poll ( read , { timeout : 10_000 } ) . toMatchObject ( {
136+ mounted : true ,
137+ collapsed : false ,
138+ hidden : false ,
139+ count : states . length ,
140+ states,
141+ } )
142+ return api
143+ } ,
144+ async expectCollapsed ( states : ComposerProbeState [ "states" ] ) {
145+ await expect . poll ( read , { timeout : 10_000 } ) . toMatchObject ( {
146+ mounted : true ,
147+ collapsed : true ,
148+ hidden : true ,
149+ count : states . length ,
150+ states,
151+ } )
152+ return api
153+ } ,
154+ async expectClosed ( ) {
155+ await expect . poll ( read , { timeout : 10_000 } ) . toMatchObject ( { mounted : false } )
156+ return api
157+ } ,
158+ async collapse ( ) {
159+ await page . locator ( sessionTodoToggleButtonSelector ) . click ( )
160+ return api
161+ } ,
162+ async expand ( ) {
163+ await page . locator ( sessionTodoToggleButtonSelector ) . click ( )
164+ return api
165+ } ,
166+ }
167+
168+ return api
169+ }
170+
62171async function withMockPermission < T > (
63172 page : any ,
64173 request : {
@@ -70,7 +179,7 @@ async function withMockPermission<T>(
70179 always ?: string [ ]
71180 } ,
72181 opts : { child ?: any } | undefined ,
73- fn : ( ) => Promise < T > ,
182+ fn : ( state : { resolved : ( ) => Promise < void > } ) => Promise < T > ,
74183) {
75184 let pending = [
76185 {
@@ -119,8 +228,14 @@ async function withMockPermission<T>(
119228
120229 if ( sessionList ) await page . route ( "**/session?*" , sessionList )
121230
231+ const state = {
232+ async resolved ( ) {
233+ await expect . poll ( ( ) => pending . length , { timeout : 10_000 } ) . toBe ( 0 )
234+ } ,
235+ }
236+
122237 try {
123- return await fn ( )
238+ return await fn ( state )
124239 } finally {
125240 await page . unroute ( "**/permission" , list )
126241 await page . unroute ( "**/session/*/permissions/*" , reply )
@@ -173,14 +288,12 @@ test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSess
173288 } )
174289
175290 const dock = page . locator ( questionDockSelector )
176- await expect . poll ( ( ) => dock . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
177- await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
291+ await expectQuestionBlocked ( page )
178292
179293 await dock . locator ( '[data-slot="question-option"]' ) . first ( ) . click ( )
180294 await dock . getByRole ( "button" , { name : / s u b m i t / i } ) . click ( )
181295
182- await expect . poll ( ( ) => page . locator ( questionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 0 )
183- await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
296+ await expectQuestionOpen ( page )
184297 } )
185298 } )
186299} )
@@ -199,15 +312,14 @@ test("blocked permission flow supports allow once", async ({ page, sdk, gotoSess
199312 metadata : { description : "Need permission for command" } ,
200313 } ,
201314 undefined ,
202- async ( ) => {
315+ async ( state ) => {
203316 await page . goto ( page . url ( ) )
204- await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
205- await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
317+ await expectPermissionBlocked ( page )
206318
207319 await clearPermissionDock ( page , / a l l o w o n c e / i)
320+ await state . resolved ( )
208321 await page . goto ( page . url ( ) )
209- await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 0 )
210- await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
322+ await expectPermissionOpen ( page )
211323 } ,
212324 )
213325 } )
@@ -226,15 +338,14 @@ test("blocked permission flow supports reject", async ({ page, sdk, gotoSession
226338 patterns : [ "/tmp/opencode-e2e-perm-reject" ] ,
227339 } ,
228340 undefined ,
229- async ( ) => {
341+ async ( state ) => {
230342 await page . goto ( page . url ( ) )
231- await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
232- await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
343+ await expectPermissionBlocked ( page )
233344
234345 await clearPermissionDock ( page , / d e n y / i)
346+ await state . resolved ( )
235347 await page . goto ( page . url ( ) )
236- await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 0 )
237- await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
348+ await expectPermissionOpen ( page )
238349 } ,
239350 )
240351 } )
@@ -254,15 +365,14 @@ test("blocked permission flow supports allow always", async ({ page, sdk, gotoSe
254365 metadata : { description : "Need permission for command" } ,
255366 } ,
256367 undefined ,
257- async ( ) => {
368+ async ( state ) => {
258369 await page . goto ( page . url ( ) )
259- await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
260- await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
370+ await expectPermissionBlocked ( page )
261371
262372 await clearPermissionDock ( page , / a l l o w a l w a y s / i)
373+ await state . resolved ( )
263374 await page . goto ( page . url ( ) )
264- await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 0 )
265- await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
375+ await expectPermissionOpen ( page )
266376 } ,
267377 )
268378 } )
@@ -301,14 +411,12 @@ test("child session question request blocks parent dock and unblocks after submi
301411 } )
302412
303413 const dock = page . locator ( questionDockSelector )
304- await expect . poll ( ( ) => dock . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
305- await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
414+ await expectQuestionBlocked ( page )
306415
307416 await dock . locator ( '[data-slot="question-option"]' ) . first ( ) . click ( )
308417 await dock . getByRole ( "button" , { name : / s u b m i t / i } ) . click ( )
309418
310- await expect . poll ( ( ) => page . locator ( questionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 0 )
311- await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
419+ await expectQuestionOpen ( page )
312420 } )
313421 } finally {
314422 await cleanupSession ( { sdk, sessionID : child . id } )
@@ -344,17 +452,15 @@ test("child session permission request blocks parent dock and supports allow onc
344452 metadata : { description : "Need child permission" } ,
345453 } ,
346454 { child } ,
347- async ( ) => {
455+ async ( state ) => {
348456 await page . goto ( page . url ( ) )
349- const dock = page . locator ( permissionDockSelector )
350- await expect . poll ( ( ) => dock . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
351- await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
457+ await expectPermissionBlocked ( page )
352458
353459 await clearPermissionDock ( page , / a l l o w o n c e / i)
460+ await state . resolved ( )
354461 await page . goto ( page . url ( ) )
355462
356- await expect . poll ( ( ) => page . locator ( permissionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 0 )
357- await expect ( page . locator ( promptSelector ) ) . toBeVisible ( )
463+ await expectPermissionOpen ( page )
358464 } ,
359465 )
360466 } finally {
@@ -365,36 +471,31 @@ test("child session permission request blocks parent dock and supports allow onc
365471
366472test ( "todo dock transitions and collapse behavior" , async ( { page, sdk, gotoSession } ) => {
367473 await withDockSession ( sdk , "e2e composer dock todo" , async ( session ) => {
368- await withDockSeed ( sdk , session . id , async ( ) => {
369- await gotoSession ( session . id )
370-
371- await seedSessionTodos ( sdk , {
372- sessionID : session . id ,
373- todos : [
374- { content : "first task" , status : "pending" , priority : "high" } ,
375- { content : "second task" , status : "in_progress" , priority : "medium" } ,
376- ] ,
377- } )
378-
379- await expect . poll ( ( ) => page . locator ( sessionTodoDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
380- await expect ( page . locator ( sessionTodoListSelector ) ) . toBeVisible ( )
381-
382- await page . locator ( sessionTodoToggleButtonSelector ) . click ( )
383- await expect ( page . locator ( sessionTodoListSelector ) ) . toBeHidden ( )
384-
385- await page . locator ( sessionTodoToggleButtonSelector ) . click ( )
386- await expect ( page . locator ( sessionTodoListSelector ) ) . toBeVisible ( )
387-
388- await seedSessionTodos ( sdk , {
389- sessionID : session . id ,
390- todos : [
391- { content : "first task" , status : "completed" , priority : "high" } ,
392- { content : "second task" , status : "cancelled" , priority : "medium" } ,
393- ] ,
394- } )
474+ const dock = await todoDock ( page , session . id )
475+ await gotoSession ( session . id )
476+ await expect ( page . locator ( sessionComposerDockSelector ) ) . toBeVisible ( )
395477
396- await expect . poll ( ( ) => page . locator ( sessionTodoDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 0 )
397- } )
478+ try {
479+ await dock . open ( [
480+ { content : "first task" , status : "pending" , priority : "high" } ,
481+ { content : "second task" , status : "in_progress" , priority : "medium" } ,
482+ ] )
483+ await dock . expectOpen ( [ "pending" , "in_progress" ] )
484+
485+ await dock . collapse ( )
486+ await dock . expectCollapsed ( [ "pending" , "in_progress" ] )
487+
488+ await dock . expand ( )
489+ await dock . expectOpen ( [ "pending" , "in_progress" ] )
490+
491+ await dock . finish ( [
492+ { content : "first task" , status : "completed" , priority : "high" } ,
493+ { content : "second task" , status : "cancelled" , priority : "medium" } ,
494+ ] )
495+ await dock . expectClosed ( )
496+ } finally {
497+ await dock . clear ( )
498+ }
398499 } )
399500} )
400501
@@ -414,8 +515,7 @@ test("keyboard focus stays off prompt while blocked", async ({ page, sdk, gotoSe
414515 ] ,
415516 } )
416517
417- await expect . poll ( ( ) => page . locator ( questionDockSelector ) . count ( ) , { timeout : 10_000 } ) . toBe ( 1 )
418- await expect ( page . locator ( promptSelector ) ) . toHaveCount ( 0 )
518+ await expectQuestionBlocked ( page )
419519
420520 await page . locator ( "main" ) . click ( { position : { x : 5 , y : 5 } } )
421521 await page . keyboard . type ( "abc" )
0 commit comments