@@ -819,3 +819,144 @@ scenario(
819819 } ) ,
820820 ) ,
821821) ;
822+
823+ // ===========================================================================
824+ // 6. Edit sheet, click interaction: the combobox popup is portaled outside the
825+ // Radix sheet, so clicking an option must not be treated as an outside
826+ // interaction that dismisses the sheet (keyboard scenarios above miss this).
827+ // ===========================================================================
828+
829+ scenario (
830+ "Health checks (UI) · clicking a combobox option in the sheet selects it without closing the sheet" ,
831+ { } ,
832+ Effect . scoped (
833+ Effect . gen ( function * ( ) {
834+ const target = yield * Target ;
835+ const browser = yield * Browser ;
836+ const { client : makeClient } = yield * Api ;
837+ const identity = yield * target . newIdentity ( ) ;
838+ const client = yield * makeClient ( api , identity ) ;
839+ const slug = newSlug ( "hc-ui-click" ) ;
840+
841+ yield * Effect . ensuring (
842+ Effect . gen ( function * ( ) {
843+ yield * registerIdentityIntegration ( client , slug , "https://identity.example.com" ) ;
844+ const operation = yield * getMeOperation ( client , slug ) ;
845+
846+ yield * browser . session ( identity , async ( { page, step } ) => {
847+ const sheet = page . getByRole ( "dialog" ) ;
848+ const operationInput = page . locator ( "#health-check-operation" ) ;
849+ const identityInput = page . locator ( "#health-check-identity" ) ;
850+
851+ await step ( "Open the health-check editor" , async ( ) => {
852+ await page . goto ( `/integrations/${ slug } ` , { waitUntil : "networkidle" } ) ;
853+ await page . getByRole ( "heading" , { level : 3 , name : "Health check" } ) . waitFor ( ) ;
854+ await page . getByRole ( "button" , { name : "Set up" } ) . click ( ) ;
855+ await operationInput . waitFor ( ) ;
856+ } ) ;
857+
858+ await step (
859+ "Click the operation option: it selects and the sheet stays open" ,
860+ async ( ) => {
861+ await operationInput . click ( ) ;
862+ await page . getByRole ( "option" ) . filter ( { hasText : "getMe" } ) . first ( ) . click ( ) ;
863+ // Clicking the portaled popup option must NOT dismiss the sheet.
864+ await sheet . waitFor ( ) ;
865+ expect ( await operationInput . inputValue ( ) ) . toContain ( "getMe" ) ;
866+ } ,
867+ ) ;
868+
869+ await step ( "Click the identity option by mouse, then save" , async ( ) => {
870+ await identityInput . click ( ) ;
871+ await page . getByRole ( "option" ) . filter ( { hasText : "email" } ) . first ( ) . click ( ) ;
872+ await sheet . waitFor ( ) ;
873+ expect ( await identityInput . inputValue ( ) ) . toBe ( "email" ) ;
874+ await page . getByRole ( "button" , { name : "Save" , exact : true } ) . click ( ) ;
875+ await operationInput . waitFor ( { state : "hidden" } ) ;
876+ } ) ;
877+ } ) ;
878+
879+ // The mouse-driven selections persisted: only possible if the option
880+ // clicks selected without dismissing the sheet first.
881+ const stored = yield * client . integrations . healthCheckGet ( { params : { slug } } ) ;
882+ expect ( stored ) . toEqual ( { operation, identityField : "email" } ) ;
883+ } ) ,
884+ client . openapi . removeSpec ( { params : { slug } } ) . pipe ( Effect . ignore ) ,
885+ ) ;
886+ } ) ,
887+ ) ,
888+ ) ;
889+
890+ // ===========================================================================
891+ // 7. Edit sheet, scroll: a modal dialog locks body scroll, which freezes the
892+ // portaled combobox list. The editor sheet is non-modal so the list scrolls.
893+ // ===========================================================================
894+
895+ scenario (
896+ "Health checks (UI) · the combobox list scrolls inside the edit sheet" ,
897+ { } ,
898+ Effect . scoped (
899+ Effect . gen ( function * ( ) {
900+ const target = yield * Target ;
901+ const browser = yield * Browser ;
902+ const { client : makeClient } = yield * Api ;
903+ const identity = yield * target . newIdentity ( ) ;
904+ const client = yield * makeClient ( api , identity ) ;
905+ const slug = newSlug ( "hc-ui-scroll" ) ;
906+
907+ yield * Effect . ensuring (
908+ Effect . gen ( function * ( ) {
909+ // Many operations so the popup list overflows and must scroll.
910+ yield * client . openapi . addSpec ( {
911+ payload : {
912+ spec : { kind : "blob" , value : largeSpec ( "https://big.example.com" ) } ,
913+ slug,
914+ baseUrl : "https://big.example.com" ,
915+ authenticationTemplate : [
916+ {
917+ slug : "apiKey" ,
918+ type : "apiKey" ,
919+ headers : { authorization : [ "Bearer " , { type : "variable" , name : "token" } ] } ,
920+ } ,
921+ ] ,
922+ } ,
923+ } ) ;
924+
925+ yield * browser . session ( identity , async ( { page, step } ) => {
926+ const operationInput = page . locator ( "#health-check-operation" ) ;
927+ const list = page . locator ( "[data-slot='combobox-list']" ) . first ( ) ;
928+
929+ await step ( "Open the operation combobox in the sheet" , async ( ) => {
930+ await page . goto ( `/integrations/${ slug } ` , { waitUntil : "networkidle" } ) ;
931+ await page . getByRole ( "heading" , { level : 3 , name : "Health check" } ) . waitFor ( ) ;
932+ await page . getByRole ( "button" , { name : "Set up" } ) . click ( ) ;
933+ await operationInput . waitFor ( ) ;
934+ await operationInput . click ( ) ;
935+ await list . waitFor ( ) ;
936+ } ) ;
937+
938+ await step ( "Wheel-scroll the list: it actually moves (not scroll-locked)" , async ( ) => {
939+ const before = await list . evaluate ( ( el ) => el . scrollTop ) ;
940+ await list . hover ( ) ;
941+ await page . mouse . wheel ( 0 , 600 ) ;
942+ // Poll: the wheel scroll must move the list (blocked → stays 0).
943+ await page . waitForFunction (
944+ ( start ) => {
945+ const el = document . querySelector ( "[data-slot='combobox-list']" ) ;
946+ return el != null && el . scrollTop > start + 20 ;
947+ } ,
948+ before ,
949+ { timeout : 5000 } ,
950+ ) ;
951+ const after = await list . evaluate ( ( el ) => el . scrollTop ) ;
952+ expect ( after , "the list scrolled past its starting offset" ) . toBeGreaterThan ( before ) ;
953+ // The sheet stayed open through the scroll interaction.
954+ await page . getByRole ( "dialog" ) . waitFor ( ) ;
955+ } ) ;
956+ } ) ;
957+ } ) ,
958+ client . openapi . removeSpec ( { params : { slug } } ) . pipe ( Effect . ignore ) ,
959+ ) ;
960+ } ) ,
961+ ) ,
962+ ) ;
0 commit comments