@@ -606,8 +606,10 @@ scenario(
606606 const suffix = uniqueSuffix ( ) ;
607607 const originalTitle = `Draft Dashboard ${ suffix } ` ;
608608 const renamedTitle = `Quarterly Dashboard ${ suffix } ` ;
609+ const listDeleteTitle = `List Card ${ suffix } ` ;
609610
610611 let artifactId : ArtifactId | undefined ;
612+ let listArtifactId : ArtifactId | undefined ;
611613
612614 yield * Effect . gen ( function * ( ) {
613615 const rendered = yield * session . call ( "create-artifact" , {
@@ -657,39 +659,111 @@ scenario(
657659 originalTitle ,
658660 ) ;
659661
662+ // A second artifact for the gallery card's own delete path: the detail
663+ // page steps below consume the renamed one, and the card's hover →
664+ // Delete affordance is a separate surface the console must keep working.
665+ const listed = yield * session . call ( "create-artifact" , {
666+ code : artifactSource ( `list-delete-${ suffix } ` ) ,
667+ title : listDeleteTitle ,
668+ description : "A dashboard the user will delete from the gallery" ,
669+ } ) ;
670+ expect ( listed . ok , `create-artifact succeeded: ${ listed . text } ` ) . toBe ( true ) ;
671+ listArtifactId = structuredOf ( listed ) . artifactId as ArtifactId ;
672+ expect ( listArtifactId , "the second artifact was persisted" ) . toBeTruthy ( ) ;
673+
660674 yield * browser . session ( identity , async ( { page, step } ) => {
661- await step ( "Delete the artifact from the list " , async ( ) => {
675+ await step ( "Delete the artifact from its gallery card " , async ( ) => {
662676 await visit ( page , "/artifacts" ) ;
677+ // Card actions reveal on hover; the card is the link's enclosing tile.
663678 const card = page . locator ( '[data-slot="artifact-card"]' ) . filter ( {
664- hasText : renamedTitle ,
679+ hasText : listDeleteTitle ,
665680 } ) ;
666681 await card . waitFor ( { timeout : 20_000 } ) ;
667682 await card . hover ( ) ;
668683 await card . getByRole ( "button" , { name : "Delete" } ) . click ( ) ;
669684
670685 const confirm = page . getByRole ( "alertdialog" ) ;
671- await confirm . getByRole ( "heading" , { name : `Delete ${ renamedTitle } ?` } ) . waitFor ( ) ;
686+ await confirm . getByRole ( "heading" , { name : `Delete ${ listDeleteTitle } ?` } ) . waitFor ( ) ;
672687 await confirm . getByRole ( "button" , { name : "Delete Artifact" } ) . click ( ) ;
673688 await confirm . waitFor ( { state : "hidden" , timeout : 20_000 } ) ;
689+ await page
690+ . getByRole ( "link" , { name : `Open artifact ${ listDeleteTitle } ` } )
691+ . waitFor ( { state : "detached" , timeout : 20_000 } ) ;
674692 } ) ;
675693
676- await step ( "The artifact is gone from the list" , async ( ) => {
677- await page
694+ let releaseListRefresh = ( ) => { } ;
695+ let markListRefreshStarted = ( ) => { } ;
696+ const listRefreshGate = new Promise < void > ( ( resolve ) => {
697+ releaseListRefresh = resolve ;
698+ } ) ;
699+ const listRefreshStarted = new Promise < void > ( ( resolve ) => {
700+ markListRefreshStarted = resolve ;
701+ } ) ;
702+
703+ await step ( "Open the artifact and delete it from its detail page" , async ( ) => {
704+ await visit ( page , "/artifacts" ) ;
705+ await page . getByRole ( "link" , { name : `Open artifact ${ renamedTitle } ` } ) . click ( ) ;
706+ await page . getByRole ( "heading" , { name : renamedTitle } ) . waitFor ( { timeout : 20_000 } ) ;
707+
708+ // Hold the post-delete list refresh open. The redirected gallery must
709+ // carry the optimistic removal across the route handoff rather than
710+ // relying on a fast canonical response to hide a stale-cache flash.
711+ await page . route ( "**/artifacts" , async ( route ) => {
712+ if ( route . request ( ) . method ( ) !== "GET" ) {
713+ await route . continue ( ) ;
714+ return ;
715+ }
716+ markListRefreshStarted ( ) ;
717+ await listRefreshGate ;
718+ await route . continue ( ) ;
719+ } ) ;
720+
721+ await page . getByRole ( "button" , { name : "Delete" } ) . click ( ) ;
722+ const confirm = page . getByRole ( "alertdialog" ) ;
723+ await confirm . getByRole ( "heading" , { name : `Delete ${ renamedTitle } ?` } ) . waitFor ( ) ;
724+ await confirm . getByRole ( "button" , { name : "Delete Artifact" } ) . click ( ) ;
725+ } ) ;
726+
727+ await step ( "The redirected gallery already omits the deleted artifact" , async ( ) => {
728+ await page . waitForURL ( ( url ) => / \/ a r t i f a c t s \/ ? $ / . test ( url . pathname ) , {
729+ timeout : 20_000 ,
730+ } ) ;
731+ await page . getByRole ( "heading" , { name : "Saved artifacts" } ) . waitFor ( { timeout : 20_000 } ) ;
732+ await listRefreshStarted ;
733+
734+ const deletedCardCount = await page
678735 . getByRole ( "link" , { name : `Open artifact ${ renamedTitle } ` } )
679- . waitFor ( { state : "detached" , timeout : 20_000 } ) ;
736+ . count ( ) ;
737+ releaseListRefresh ( ) ;
738+ await page . unrouteAll ( { behavior : "wait" } ) ;
739+
740+ expect (
741+ deletedCardCount ,
742+ "the optimistic delete survives navigation while the list refresh is pending" ,
743+ ) . toBe ( 0 ) ;
680744 } ) ;
681745 } ) ;
682746
683747 const afterDelete = yield * session . call ( "list-artifacts" , { } ) ;
684748 expect ( afterDelete . text , "the agent no longer offers the deleted artifact" ) . not . toContain (
685749 renamedTitle ,
686750 ) ;
751+ expect ( afterDelete . text , "nor the artifact deleted from its gallery card" ) . not . toContain (
752+ listDeleteTitle ,
753+ ) ;
687754
688755 const missing = yield * session . call ( "show-artifact" , { id : artifactId } ) ;
689756 expect ( missing . ok , "fetching a deleted artifact is an error, not an empty render" ) . toBe (
690757 false ,
691758 ) ;
692759 } ) . pipe (
760+ Effect . ensuring (
761+ Effect . suspend ( ( ) =>
762+ listArtifactId === undefined
763+ ? Effect . void
764+ : client . artifacts . remove ( { params : { artifactId : listArtifactId } } ) ,
765+ ) . pipe ( Effect . ignore ) ,
766+ ) ,
693767 Effect . ensuring (
694768 Effect . suspend ( ( ) =>
695769 artifactId === undefined
0 commit comments