@@ -20,6 +20,7 @@ import {
2020 LoroCounter ,
2121 JsonDiff ,
2222 redactJsonUpdates ,
23+ UndoManager ,
2324} from "../bundler/index" ;
2425
2526it ( "basic example" , ( ) => {
@@ -145,6 +146,46 @@ describe("map", () => {
145146 } ) ;
146147} ) ;
147148
149+ it ( "top undo/redo values" , ( ) => {
150+ const doc = new LoroDoc ( ) ;
151+ doc . setPeerId ( 1 ) ;
152+
153+ let lastCommitLabel : string | null = null ;
154+ let lastPoppedLabel : string | null = null ;
155+
156+ const undo = new UndoManager ( doc , {
157+ mergeInterval : 0 ,
158+ onPush : ( isUndo : boolean ) => {
159+ // For normal commits (Undo stack), use the label set before commit.
160+ // For opposite pushes (Redo stack), reuse the last popped label.
161+ const value = isUndo ? lastCommitLabel : lastPoppedLabel ;
162+ return { value, cursors : [ ] } ;
163+ } ,
164+ onPop : ( _isUndo : boolean , meta : { value : unknown } ) => {
165+ // Remember last popped label so we can assign it to the opposite stack item.
166+ lastPoppedLabel = meta . value as string | null ;
167+ } ,
168+ } ) ;
169+
170+ // 1st commit
171+ doc . getText ( "text" ) . insert ( 0 , "A" ) ;
172+ lastCommitLabel = 'Insert "A"' ;
173+ doc . commit ( ) ;
174+ expect ( undo . topUndoValue ( ) ) . toBe ( 'Insert "A"' ) ;
175+ expect ( undo . topRedoValue ( ) ) . toBe ( undefined ) ;
176+
177+ // 2nd commit
178+ doc . getText ( "text" ) . insert ( 1 , "B" ) ;
179+ lastCommitLabel = 'Insert "B"' ;
180+ doc . commit ( ) ;
181+ expect ( undo . topUndoValue ( ) ) . toBe ( 'Insert "B"' ) ;
182+
183+ // Undo once: the popped undo label should appear as the redo top value
184+ undo . undo ( ) ;
185+ expect ( undo . topRedoValue ( ) ) . toBe ( 'Insert "B"' ) ;
186+ expect ( undo . topUndoValue ( ) ) . toBe ( 'Insert "A"' ) ;
187+ } ) ;
188+
148189describe ( "import" , ( ) => {
149190 it ( "pending and import status" , ( ) => {
150191 const a = new LoroDoc ( ) ;
0 commit comments