@@ -6,35 +6,60 @@ import { useElementLifecycleOps } from "./useElementLifecycleOps";
66import { makeLifecycleOpsParams } from "./elementLifecycleOpsTestUtils" ;
77import { mountReactHarness , makeSelection } from "./domSelectionTestHarness" ;
88
9+ Reflect . set ( globalThis , "IS_REACT_ACT_ENVIRONMENT" , true ) ;
10+
911function selectionFor ( id : string ) {
1012 const el = document . createElement ( "div" ) ;
1113 el . id = id ;
1214 document . body . append ( el ) ;
1315 return { ...makeSelection ( id , el ) , sourceFile : "index.html" } ;
1416}
1517
18+ function mountDeleteOps ( overrides : Partial < Parameters < typeof useElementLifecycleOps > [ 0 ] > = { } ) {
19+ const captured : { ops : ReturnType < typeof useElementLifecycleOps > | null } = { ops : null } ;
20+ function Probe ( ) {
21+ captured . ops = useElementLifecycleOps (
22+ makeLifecycleOpsParams ( {
23+ commitDomEditPatchBatches : vi . fn ( async ( ) => ( { ok : true } ) as never ) ,
24+ ...overrides ,
25+ } ) ,
26+ ) ;
27+ return null ;
28+ }
29+ mountReactHarness ( < Probe /> ) ;
30+ if ( ! captured . ops ) throw new Error ( "hook did not initialize" ) ;
31+ return captured . ops ;
32+ }
33+
1634describe ( "useElementLifecycleOps — deleting a canvas multi-selection" , ( ) => {
1735 const removed : string [ ] = [ ] ;
1836 const requests : string [ ] = [ ] ;
1937 let changes = true ;
38+ let removeOk = true ;
2039
2140 beforeEach ( ( ) => {
2241 removed . length = 0 ;
2342 requests . length = 0 ;
2443 changes = true ;
44+ removeOk = true ;
2545 vi . stubGlobal (
2646 "fetch" ,
2747 vi . fn ( async ( url : string , init ?: RequestInit ) => {
28- requests . push ( String ( url ) ) ;
48+ const requestUrl = String ( url ) ;
49+ requests . push ( requestUrl ) ;
2950 const body = JSON . parse ( String ( init ?. body ?? "{}" ) ) as {
3051 targets ?: { id ?: string ; selector ?: string } [ ] ;
3152 } ;
32- for ( const target of body . targets ?? [ ] ) {
33- const key = target . id ?? target . selector ;
34- if ( key ) removed . push ( key ) ;
35- }
53+ const keys = ( body . targets ?? [ ] )
54+ . map ( ( target ) => target . id ?? target . selector )
55+ . filter ( ( key ) : key is string => key !== undefined ) ;
56+ removed . push ( ...keys ) ;
57+ const isRemove = requestUrl . includes ( "/file-mutations/remove-elements/" ) ;
58+ const status = isRemove && ! removeOk ? 500 : 200 ;
3659 return {
37- ok : true ,
60+ ok : status === 200 ,
61+ status,
62+ text : async ( ) => ( status === 200 ? "" : "server said no" ) ,
3863 json : async ( ) => ( { changed : changes , content : "<html></html>" } ) ,
3964 } as unknown as Response ;
4065 } ) ,
@@ -48,28 +73,62 @@ describe("useElementLifecycleOps — deleting a canvas multi-selection", () => {
4873 it ( "removes every selected element, not just the first" , async ( ) => {
4974 // The reported bug: select several elements on the canvas, press Delete, and
5075 // one disappears while the rest stay — still drawn as selected.
51- let ops : ReturnType < typeof useElementLifecycleOps > | null = null ;
52- function Probe ( ) {
53- ops = useElementLifecycleOps (
54- makeLifecycleOpsParams ( {
55- commitDomEditPatchBatches : vi . fn ( async ( ) => ( { ok : true } ) as never ) ,
56- projectIdRef : { current : "p1" } ,
57- } ) ,
58- ) ;
59- return null ;
60- }
61- mountReactHarness ( < Probe /> ) ;
76+ const ops = mountDeleteOps ( { projectIdRef : { current : "p1" } } ) ;
6277
6378 const selections = [ "a" , "b" , "c" ] . map ( selectionFor ) ;
79+ let outcome : unknown ;
6480 await act ( async ( ) => {
65- await ops ! . handleDomEditElementsDelete ( selections ) ;
81+ outcome = await ops . handleDomEditElementsDelete ( selections ) ;
6682 } ) ;
6783
6884 // The defect: only the first was ever removed.
6985 expect ( removed ) . toEqual ( [ "a" , "b" , "c" ] ) ;
7086 // And one request for the selection, not one per member: a canvas selection
7187 // runs to hundreds, and a round trip each made Delete look like a no-op.
7288 expect ( requests . filter ( ( url ) => url . includes ( "remove-elements" ) ) ) . toHaveLength ( 1 ) ;
89+ expect ( outcome ) . toEqual ( { ok : true } ) ;
90+ } ) ;
91+
92+ it ( "reports a successful SDK delete as landed" , async ( ) => {
93+ const ops = mountDeleteOps ( {
94+ projectIdRef : { current : "p1" } ,
95+ onTrySdkDelete : vi . fn ( async ( ) => ( { status : "committed" , version : "v1" } ) as const ) ,
96+ } ) ;
97+
98+ const target = { ...selectionFor ( "a" ) , hfId : "hf-a" } ;
99+ let outcome : unknown ;
100+ await act ( async ( ) => {
101+ outcome = await ops . handleDomEditElementsDelete ( [ target ] ) ;
102+ } ) ;
103+
104+ expect ( outcome ) . toEqual ( { ok : true } ) ;
105+ expect ( requests . some ( ( url ) => url . includes ( "remove-elements" ) ) ) . toBe ( false ) ;
106+ } ) ;
107+
108+ it ( "reports missing project and selection without starting a request" , async ( ) => {
109+ const projectIdRef = { current : null as string | null } ;
110+ const ops = mountDeleteOps ( { projectIdRef } ) ;
111+
112+ await expect ( ops . handleDomEditElementsDelete ( [ selectionFor ( "a" ) ] ) ) . resolves . toEqual ( {
113+ ok : false ,
114+ reason : "no-project" ,
115+ } ) ;
116+ projectIdRef . current = "p1" ;
117+ await expect ( ops . handleDomEditElementsDelete ( [ ] ) ) . resolves . toEqual ( {
118+ ok : false ,
119+ reason : "no-selection" ,
120+ } ) ;
121+ expect ( requests ) . toEqual ( [ ] ) ;
122+ } ) ;
123+
124+ it ( "reports an HTTP write failure instead of only toasting" , async ( ) => {
125+ removeOk = false ;
126+ const ops = mountDeleteOps ( { projectIdRef : { current : "p1" } } ) ;
127+
128+ await expect ( ops . handleDomEditElementsDelete ( [ selectionFor ( "a" ) ] ) ) . resolves . toEqual ( {
129+ ok : false ,
130+ reason : "persist-failed" ,
131+ } ) ;
73132 } ) ;
74133
75134 it ( "says so when the preview is stale instead of claiming a delete" , async ( ) => {
@@ -78,23 +137,14 @@ describe("useElementLifecycleOps — deleting a canvas multi-selection", () => {
78137 // nothing at all, with nothing on screen to explain it.
79138 changes = false ;
80139 const showToast = vi . fn ( ) ;
81- let ops : ReturnType < typeof useElementLifecycleOps > | null = null ;
82- function Probe ( ) {
83- ops = useElementLifecycleOps (
84- makeLifecycleOpsParams ( {
85- commitDomEditPatchBatches : vi . fn ( async ( ) => ( { ok : true } ) as never ) ,
86- projectIdRef : { current : "p1" } ,
87- showToast,
88- } ) ,
89- ) ;
90- return null ;
91- }
92- mountReactHarness ( < Probe /> ) ;
140+ const ops = mountDeleteOps ( { projectIdRef : { current : "p1" } , showToast } ) ;
93141
142+ let outcome : unknown ;
94143 await act ( async ( ) => {
95- await ops ! . handleDomEditElementsDelete ( [ selectionFor ( "a" ) ] ) ;
144+ outcome = await ops . handleDomEditElementsDelete ( [ selectionFor ( "a" ) ] ) ;
96145 } ) ;
97146
98147 expect ( showToast . mock . calls . flat ( ) . join ( " " ) ) . toContain ( "out of date" ) ;
148+ expect ( outcome ) . toEqual ( { ok : false , reason : "persist-failed" } ) ;
99149 } ) ;
100150} ) ;
0 commit comments