@@ -330,6 +330,137 @@ describe('tables-adapter regressions', () => {
330330 expect ( tr . insert ) . toHaveBeenCalledWith ( expectedInsertPos , expect . anything ( ) ) ;
331331 } ) ;
332332
333+ it ( 'deletes shiftLeft cells without appending a trailing replacement cell' , ( ) => {
334+ const editor = makeTableEditor ( ) ;
335+ const tr = editor . state . tr as unknown as {
336+ delete : ReturnType < typeof vi . fn > ;
337+ insert : ReturnType < typeof vi . fn > ;
338+ setNodeMarkup : ReturnType < typeof vi . fn > ;
339+ } ;
340+ const tableNode = editor . state . doc . nodeAt ( 0 ) as ProseMirrorNode ;
341+ const targetCellOffset = TableMap . get ( tableNode ) . map [ 0 ] ! ;
342+ const targetCellNode = tableNode . nodeAt ( targetCellOffset ) as ProseMirrorNode ;
343+ const expectedStart = 1 + targetCellOffset ;
344+ const expectedEnd = expectedStart + targetCellNode . nodeSize ;
345+
346+ const result = tablesDeleteCellAdapter ( editor , { nodeId : 'cell-1' , mode : 'shiftLeft' } ) ;
347+ expect ( result . success ) . toBe ( true ) ;
348+ expect ( tr . delete ) . toHaveBeenCalledWith ( expectedStart , expectedEnd ) ;
349+ expect ( tr . insert ) . not . toHaveBeenCalled ( ) ;
350+ expect ( tr . setNodeMarkup ) . toHaveBeenCalledWith (
351+ expect . any ( Number ) ,
352+ null ,
353+ expect . objectContaining ( {
354+ colspan : 2 ,
355+ } ) ,
356+ ) ;
357+ } ) ;
358+
359+ it ( 'deletes the row trailing cell for shiftLeft without appending a replacement cell' , ( ) => {
360+ const editor = makeTableEditor ( ) ;
361+ const tr = editor . state . tr as unknown as {
362+ delete : ReturnType < typeof vi . fn > ;
363+ insert : ReturnType < typeof vi . fn > ;
364+ setNodeMarkup : ReturnType < typeof vi . fn > ;
365+ } ;
366+ const tableNode = editor . state . doc . nodeAt ( 0 ) as ProseMirrorNode ;
367+ const targetCellOffset = TableMap . get ( tableNode ) . map [ 1 ] ! ;
368+ const targetCellNode = tableNode . nodeAt ( targetCellOffset ) as ProseMirrorNode ;
369+ const expectedStart = 1 + targetCellOffset ;
370+ const expectedEnd = expectedStart + targetCellNode . nodeSize ;
371+
372+ const result = tablesDeleteCellAdapter ( editor , { nodeId : 'cell-2' , mode : 'shiftLeft' } ) ;
373+ expect ( result . success ) . toBe ( true ) ;
374+ expect ( tr . delete ) . toHaveBeenCalledWith ( expectedStart , expectedEnd ) ;
375+ expect ( tr . insert ) . not . toHaveBeenCalled ( ) ;
376+ expect ( tr . setNodeMarkup ) . toHaveBeenCalledWith (
377+ expect . any ( Number ) ,
378+ null ,
379+ expect . objectContaining ( {
380+ colspan : 2 ,
381+ } ) ,
382+ ) ;
383+ } ) ;
384+
385+ it ( 'falls back to trailing replacement cell when shiftLeft would widen a vertically merged trailing cell' , ( ) => {
386+ const editor = makeTableEditor ( ) ;
387+ const tr = editor . state . tr as unknown as {
388+ delete : ReturnType < typeof vi . fn > ;
389+ insert : ReturnType < typeof vi . fn > ;
390+ setNodeMarkup : ReturnType < typeof vi . fn > ;
391+ } ;
392+ const tableNode = editor . state . doc . nodeAt ( 0 ) as ProseMirrorNode ;
393+ const firstRow = tableNode . child ( 0 ) as ProseMirrorNode ;
394+ const trailingCell = firstRow . child ( 1 ) as unknown as { attrs : Record < string , unknown > } ;
395+ trailingCell . attrs . rowspan = 2 ;
396+ trailingCell . attrs . tableCellProperties = { vMerge : 'restart' } ;
397+
398+ const result = tablesDeleteCellAdapter ( editor , { nodeId : 'cell-1' , mode : 'shiftLeft' } ) ;
399+ expect ( result . success ) . toBe ( true ) ;
400+ expect ( tr . insert ) . toHaveBeenCalledWith ( expect . any ( Number ) , expect . anything ( ) ) ;
401+ expect ( tr . setNodeMarkup ) . not . toHaveBeenCalled ( ) ;
402+ } ) ;
403+
404+ it ( 'shiftLeft vMerge fallback inserts at the post-delete row end without double-mapping' , ( ) => {
405+ // Regression: rowEndPos was computed from the post-delete doc (tr.doc) but then
406+ // passed through tr.mapping.map() which maps old→new, double-shifting the position.
407+ const editor = makeTableEditor ( ) ;
408+
409+ const tableNode = editor . state . doc . nodeAt ( 0 ) as ProseMirrorNode ;
410+ const firstRow = tableNode . child ( 0 ) as ProseMirrorNode ;
411+ const trailingCell = firstRow . child ( 1 ) as unknown as { attrs : Record < string , unknown > } ;
412+ trailingCell . attrs . rowspan = 2 ;
413+ trailingCell . attrs . tableCellProperties = { vMerge : 'restart' } ;
414+
415+ const cell1 = firstRow . child ( 0 ) ;
416+ const deletionStart = 2 ; // absolute position of cell-1
417+ const deletionSize = cell1 . nodeSize ; // 9
418+
419+ // Build post-delete table: row 0 only contains the vMerge cell.
420+ const postDeleteRow0 = createNode ( 'tableRow' , [ firstRow . child ( 1 ) ] , {
421+ attrs : { ...( firstRow . attrs as Record < string , unknown > ) } ,
422+ isBlock : true ,
423+ inlineContent : false ,
424+ } ) ;
425+ const postDeleteTable = createNode ( 'table' , [ postDeleteRow0 , tableNode . child ( 1 ) ] , {
426+ attrs : { ...( tableNode . attrs as Record < string , unknown > ) } ,
427+ isBlock : true ,
428+ inlineContent : false ,
429+ } ) ;
430+ const postDeleteDoc = createNode ( 'doc' , [ postDeleteTable ] , { isBlock : false } ) ;
431+
432+ const trObj = editor . state . tr as unknown as {
433+ delete : ReturnType < typeof vi . fn > ;
434+ insert : ReturnType < typeof vi . fn > ;
435+ mapping : { map : ( p : number ) => number ; maps : unknown [ ] ; slice : ( ) => { map : ( p : number ) => number } } ;
436+ doc : ProseMirrorNode ;
437+ } ;
438+
439+ // Swap tr.doc to the post-delete document when delete is called.
440+ trObj . delete = vi . fn ( ( ) => {
441+ trObj . doc = postDeleteDoc ;
442+ return trObj ;
443+ } ) ;
444+
445+ // Simulate real deletion mapping: positions at or after the deleted range shift left.
446+ trObj . mapping . map = ( pos : number ) => {
447+ if ( pos < deletionStart ) return pos ;
448+ if ( pos < deletionStart + deletionSize ) return deletionStart ;
449+ return pos - deletionSize ;
450+ } ;
451+
452+ const result = tablesDeleteCellAdapter ( editor , { nodeId : 'cell-1' , mode : 'shiftLeft' } ) ;
453+ expect ( result . success ) . toBe ( true ) ;
454+ expect ( trObj . insert ) . toHaveBeenCalled ( ) ;
455+
456+ // Post-delete row 0 nodeSize = 2 + cell-2 size (9) = 11.
457+ // rowEndPos = tablePos(0) + 1 + 11 = 12.
458+ // Correct insert = 12 - 1 = 11 (just inside the row).
459+ // Old buggy code: tr.mapping.map(11) = 11 - 9 = 2 — wrong position!
460+ const insertPos = trObj . insert . mock . calls [ 0 ] ! [ 0 ] ;
461+ expect ( insertPos ) . toBe ( 11 ) ;
462+ } ) ;
463+
333464 it ( 'keeps table grid widths in sync when distributing columns' , ( ) => {
334465 const editor = makeTableEditor ( ) ;
335466 const tr = editor . state . tr as unknown as { setNodeMarkup : ReturnType < typeof vi . fn > } ;
0 commit comments