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