11import { base64Decode , base64Encode } from "@opencode-ai/util/encode"
2- import { expect , type Locator , type Page , type Route } from "@playwright/test"
2+ import { expect , type Locator , type Page } from "@playwright/test"
33import fs from "node:fs/promises"
44import os from "node:os"
55import path from "node:path"
66import { execSync } from "node:child_process"
77import { terminalAttr , type E2EWindow } from "../src/testing/terminal"
88import { createSdk , modKey , resolveDirectory , serverUrl } from "./utils"
99import {
10- dropdownMenuTriggerSelector ,
1110 dropdownMenuContentSelector ,
1211 projectSwitchSelector ,
1312 projectMenuTriggerSelector ,
@@ -43,27 +42,6 @@ export async function defocus(page: Page) {
4342 . catch ( ( ) => undefined )
4443}
4544
46- export async function withNoReplyPrompt < T > ( page : Page , fn : ( ) => Promise < T > ) {
47- const url = "**/session/*/prompt_async"
48- const route = async ( input : Route ) => {
49- const body = input . request ( ) . postDataJSON ( )
50- await input . continue ( {
51- postData : JSON . stringify ( { ...body , noReply : true } ) ,
52- headers : {
53- ...input . request ( ) . headers ( ) ,
54- "content-type" : "application/json" ,
55- } ,
56- } )
57- }
58-
59- await page . route ( url , route )
60- try {
61- return await fn ( )
62- } finally {
63- await page . unroute ( url , route )
64- }
65- }
66-
6745async function terminalID ( term : Locator ) {
6846 const id = await term . getAttribute ( terminalAttr )
6947 if ( id ) return id
@@ -333,63 +311,6 @@ export async function openSettings(page: Page) {
333311 return dialog
334312}
335313
336- export async function seedProjects ( page : Page , input : { directory : string ; extra ?: string [ ] ; serverUrl ?: string } ) {
337- await page . addInitScript (
338- ( args : { directory : string ; serverUrl : string ; extra : string [ ] } ) => {
339- const key = "opencode.global.dat:server"
340- const defaultKey = "opencode.settings.dat:defaultServerUrl"
341- const raw = localStorage . getItem ( key )
342- const parsed = ( ( ) => {
343- if ( ! raw ) return undefined
344- try {
345- return JSON . parse ( raw ) as unknown
346- } catch {
347- return undefined
348- }
349- } ) ( )
350-
351- const store = parsed && typeof parsed === "object" ? ( parsed as Record < string , unknown > ) : { }
352- const list = Array . isArray ( store . list ) ? store . list : [ ]
353- const lastProject = store . lastProject && typeof store . lastProject === "object" ? store . lastProject : { }
354- const projects = store . projects && typeof store . projects === "object" ? store . projects : { }
355- const nextProjects = { ...( projects as Record < string , unknown > ) }
356- const nextList = list . includes ( args . serverUrl ) ? list : [ args . serverUrl , ...list ]
357-
358- const add = ( origin : string , directory : string ) => {
359- const current = nextProjects [ origin ]
360- const items = Array . isArray ( current ) ? current : [ ]
361- const existing = items . filter (
362- ( p ) : p is { worktree : string ; expanded ?: boolean } =>
363- ! ! p &&
364- typeof p === "object" &&
365- "worktree" in p &&
366- typeof ( p as { worktree ?: unknown } ) . worktree === "string" ,
367- )
368-
369- if ( existing . some ( ( p ) => p . worktree === directory ) ) return
370- nextProjects [ origin ] = [ { worktree : directory , expanded : true } , ...existing ]
371- }
372-
373- const directories = [ args . directory , ...args . extra ]
374- for ( const directory of directories ) {
375- add ( "local" , directory )
376- add ( args . serverUrl , directory )
377- }
378-
379- localStorage . setItem (
380- key ,
381- JSON . stringify ( {
382- list : nextList ,
383- projects : nextProjects ,
384- lastProject,
385- } ) ,
386- )
387- localStorage . setItem ( defaultKey , args . serverUrl )
388- } ,
389- { directory : input . directory , serverUrl : input . serverUrl ?? serverUrl , extra : input . extra ?? [ ] } ,
390- )
391- }
392-
393314export async function createTestProject ( input ?: { serverUrl ?: string } ) {
394315 const root = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "opencode-e2e-project-" ) )
395316 const id = `e2e-${ path . basename ( root ) } `
@@ -479,7 +400,15 @@ export async function waitDir(page: Page, directory: string, input?: { serverUrl
479400 return { directory : target , slug : base64Encode ( target ) }
480401}
481402
482- export async function waitSession ( page : Page , input : { directory : string ; sessionID ?: string ; serverUrl ?: string } ) {
403+ export async function waitSession (
404+ page : Page ,
405+ input : {
406+ directory : string
407+ sessionID ?: string
408+ serverUrl ?: string
409+ allowAnySession ?: boolean
410+ } ,
411+ ) {
483412 const target = await resolveDirectory ( input . directory , input . serverUrl )
484413 await expect
485414 . poll (
@@ -491,11 +420,11 @@ export async function waitSession(page: Page, input: { directory: string; sessio
491420 if ( ! resolved || resolved . directory !== target ) return false
492421 const current = sessionIDFromUrl ( page . url ( ) )
493422 if ( input . sessionID && current !== input . sessionID ) return false
494- if ( ! input . sessionID && current ) return false
423+ if ( ! input . sessionID && ! input . allowAnySession && current ) return false
495424
496425 const state = await probeSession ( page )
497426 if ( input . sessionID && ( ! state || state . sessionID !== input . sessionID ) ) return false
498- if ( ! input . sessionID && state ?. sessionID ) return false
427+ if ( ! input . sessionID && ! input . allowAnySession && state ?. sessionID ) return false
499428 if ( state ?. dir ) {
500429 const dir = await resolveDirectory ( state . dir , input . serverUrl ) . catch ( ( ) => state . dir ?? "" )
501430 if ( dir !== target ) return false
@@ -602,12 +531,15 @@ export async function confirmDialog(page: Page, buttonName: string | RegExp) {
602531}
603532
604533export async function openSharePopover ( page : Page ) {
605- const rightSection = page . locator ( titlebarRightSelector )
606- const shareButton = rightSection . getByRole ( "button" , { name : "Share" } ) . first ( )
607- await expect ( shareButton ) . toBeVisible ( )
534+ const scroller = page . locator ( ".scroll-view__viewport" ) . first ( )
535+ await expect ( scroller ) . toBeVisible ( )
536+ await expect ( scroller . getByRole ( "heading" , { level : 1 } ) . first ( ) ) . toBeVisible ( { timeout : 30_000 } )
537+
538+ const menuTrigger = scroller . getByRole ( "button" , { name : / m o r e o p t i o n s / i } ) . first ( )
539+ await expect ( menuTrigger ) . toBeVisible ( { timeout : 30_000 } )
608540
609541 const popoverBody = page
610- . locator ( popoverBodySelector )
542+ . locator ( '[data-component="popover-content"]' )
611543 . filter ( { has : page . getByRole ( "button" , { name : / ^ ( P u b l i s h | U n p u b l i s h ) $ / } ) } )
612544 . first ( )
613545
@@ -617,16 +549,13 @@ export async function openSharePopover(page: Page) {
617549 . catch ( ( ) => false )
618550
619551 if ( ! opened ) {
620- await shareButton . click ( )
621- await expect ( popoverBody ) . toBeVisible ( )
552+ const menu = page . locator ( dropdownMenuContentSelector ) . first ( )
553+ await menuTrigger . click ( )
554+ await clickMenuItem ( menu , / s h a r e / i)
555+ await expect ( menu ) . toHaveCount ( 0 )
556+ await expect ( popoverBody ) . toBeVisible ( { timeout : 30_000 } )
622557 }
623- return { rightSection, popoverBody }
624- }
625-
626- export async function clickPopoverButton ( page : Page , buttonName : string | RegExp ) {
627- const button = page . getByRole ( "button" ) . filter ( { hasText : buttonName } ) . first ( )
628- await expect ( button ) . toBeVisible ( )
629- await button . click ( )
558+ return { rightSection : scroller , popoverBody }
630559}
631560
632561export async function clickListItem (
@@ -794,40 +723,6 @@ export async function seedSessionQuestion(
794723 return { id : result . id }
795724}
796725
797- export async function seedSessionPermission (
798- sdk : ReturnType < typeof createSdk > ,
799- input : {
800- sessionID : string
801- permission : string
802- patterns : string [ ]
803- description ?: string
804- } ,
805- ) {
806- const text = [
807- "Your only valid response is one bash tool call." ,
808- `Use this JSON input: ${ JSON . stringify ( {
809- command : input . patterns [ 0 ] ? `ls ${ JSON . stringify ( input . patterns [ 0 ] ) } ` : "pwd" ,
810- workdir : "/" ,
811- description : input . description ?? `seed ${ input . permission } permission request` ,
812- } ) } `,
813- "Do not output plain text." ,
814- ] . join ( "\n" )
815-
816- const result = await seed ( {
817- sdk,
818- sessionID : input . sessionID ,
819- prompt : text ,
820- timeout : 30_000 ,
821- probe : async ( ) => {
822- const list = await sdk . permission . list ( ) . then ( ( x ) => x . data ?? [ ] )
823- return list . find ( ( item ) => item . sessionID === input . sessionID )
824- } ,
825- } )
826-
827- if ( ! result ) throw new Error ( "Timed out seeding permission request" )
828- return { id : result . id }
829- }
830-
831726export async function seedSessionTask (
832727 sdk : ReturnType < typeof createSdk > ,
833728 input : {
@@ -886,36 +781,6 @@ export async function seedSessionTask(
886781 return result
887782}
888783
889- export async function seedSessionTodos (
890- sdk : ReturnType < typeof createSdk > ,
891- input : {
892- sessionID : string
893- todos : Array < { content : string ; status : string ; priority : string } >
894- } ,
895- ) {
896- const text = [
897- "Your only valid response is one todowrite tool call." ,
898- `Use this JSON input: ${ JSON . stringify ( { todos : input . todos } ) } ` ,
899- "Do not output plain text." ,
900- ] . join ( "\n" )
901- const target = JSON . stringify ( input . todos )
902-
903- const result = await seed ( {
904- sdk,
905- sessionID : input . sessionID ,
906- prompt : text ,
907- timeout : 30_000 ,
908- probe : async ( ) => {
909- const todos = await sdk . session . todo ( { sessionID : input . sessionID } ) . then ( ( x ) => x . data ?? [ ] )
910- if ( JSON . stringify ( todos ) !== target ) return
911- return true
912- } ,
913- } )
914-
915- if ( ! result ) throw new Error ( "Timed out seeding todos" )
916- return true
917- }
918-
919784export async function clearSessionDockSeed ( sdk : ReturnType < typeof createSdk > , sessionID : string ) {
920785 const [ questions , permissions ] = await Promise . all ( [
921786 sdk . question . list ( ) . then ( ( x ) => x . data ?? [ ] ) ,
@@ -1005,30 +870,57 @@ export async function openProjectMenu(page: Page, projectSlug: string) {
1005870}
1006871
1007872export async function setWorkspacesEnabled ( page : Page , projectSlug : string , enabled : boolean ) {
1008- const current = await page
1009- . getByRole ( "button" , { name : "New workspace" } )
1010- . first ( )
1011- . isVisible ( )
1012- . then ( ( x ) => x )
1013- . catch ( ( ) => false )
873+ const current = ( ) =>
874+ page
875+ . getByRole ( "button" , { name : "New workspace" } )
876+ . first ( )
877+ . isVisible ( )
878+ . then ( ( x ) => x )
879+ . catch ( ( ) => false )
880+
881+ if ( ( await current ( ) ) === enabled ) return
1014882
1015- if ( current === enabled ) return
883+ if ( enabled ) {
884+ await page . reload ( )
885+ await openSidebar ( page )
886+ if ( ( await current ( ) ) === enabled ) return
887+ }
1016888
1017889 const flip = async ( timeout ?: number ) => {
1018890 const menu = await openProjectMenu ( page , projectSlug )
1019891 const toggle = menu . locator ( projectWorkspacesToggleSelector ( projectSlug ) ) . first ( )
1020892 await expect ( toggle ) . toBeVisible ( )
1021- return toggle . click ( { force : true , timeout } )
893+ await expect ( toggle ) . toBeEnabled ( { timeout : 30_000 } )
894+ const clicked = await toggle
895+ . click ( { force : true , timeout } )
896+ . then ( ( ) => true )
897+ . catch ( ( ) => false )
898+ if ( clicked ) return
899+ await toggle . focus ( )
900+ await page . keyboard . press ( "Enter" )
1022901 }
1023902
1024- const flipped = await flip ( 1500 )
1025- . then ( ( ) => true )
1026- . catch ( ( ) => false )
903+ for ( const timeout of [ 1500 , undefined , undefined ] ) {
904+ if ( ( await current ( ) ) === enabled ) break
905+ await flip ( timeout )
906+ . then ( ( ) => undefined )
907+ . catch ( ( ) => undefined )
908+ const matched = await expect
909+ . poll ( current , { timeout : 5_000 } )
910+ . toBe ( enabled )
911+ . then ( ( ) => true )
912+ . catch ( ( ) => false )
913+ if ( matched ) break
914+ }
1027915
1028- if ( ! flipped ) await flip ( )
916+ if ( ( await current ( ) ) !== enabled ) {
917+ await page . reload ( )
918+ await openSidebar ( page )
919+ }
1029920
1030921 const expected = enabled ? "New workspace" : "New session"
1031- await expect ( page . getByRole ( "button" , { name : expected } ) . first ( ) ) . toBeVisible ( )
922+ await expect . poll ( current , { timeout : 60_000 } ) . toBe ( enabled )
923+ await expect ( page . getByRole ( "button" , { name : expected } ) . first ( ) ) . toBeVisible ( { timeout : 30_000 } )
1032924}
1033925
1034926export async function openWorkspaceMenu ( page : Page , workspaceSlug : string ) {
0 commit comments