diff --git a/src/plugin/operationHistory.ts b/src/plugin/operationHistory.ts index 83366fb3..0c5ce3a6 100644 --- a/src/plugin/operationHistory.ts +++ b/src/plugin/operationHistory.ts @@ -88,6 +88,12 @@ export default function (mei: MindElixirInstance) { } } } + mei.clearHistory = function () { + history = [] + currentIndex = -1 + current = mei.getData() + mei.clearSelection() + } const handleOperation = function (operation: Operation) { if (operation.name === 'beginEdit') return history = history.slice(0, currentIndex + 1) diff --git a/src/types/index.ts b/src/types/index.ts index ef130cf7..4d7f57a9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -121,6 +121,15 @@ export interface MindElixirInstance extends Omit, 'markdown' | history: Operation[] undo: () => void redo: () => void + /** + * Reset the undo/redo stack and update the internal baseline snapshot to the + * current diagram state. Call this after loading new data into an existing + * instance (e.g. after `refresh()`) to prevent users from undoing back into + * a previously loaded diagram. + * + * Only available when `allowUndo` is `true` (the default). + */ + clearHistory?: () => void selection: SelectionArea dragMoveHelper: ReturnType diff --git a/tests/clear-history.spec.ts b/tests/clear-history.spec.ts new file mode 100644 index 00000000..ca738df5 --- /dev/null +++ b/tests/clear-history.spec.ts @@ -0,0 +1,105 @@ +import { test, expect } from './mind-elixir-test' + +const diagramA = { + nodeData: { + id: 'root-a', + topic: 'Diagram A', + children: [{ id: 'child-a', topic: 'Child A' }], + }, +} + +const diagramB = { + nodeData: { + id: 'root-b', + topic: 'Diagram B', + children: [{ id: 'child-b', topic: 'Child B' }], + }, +} + +test.beforeEach(async ({ me }) => { + await me.init(diagramA) +}) + +test('clearHistory - undo cannot revert into pre-refresh diagram', async ({ page, me }) => { + // Perform an operation in Diagram A + await me.click('Child A') + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + await expect(me.getByText('New Node')).toBeVisible() + + // Load Diagram B and clear the history stack + await page.evaluate((data: typeof diagramB) => { + const mind = (window as any)['#map'] + mind.refresh(data) + mind.clearHistory() + }, diagramB) + + await expect(me.getByText('Diagram B')).toBeVisible() + await expect(me.getByText('Diagram A')).toBeHidden() + + // Undo should be a no-op — must not travel back into Diagram A + await page.keyboard.press('Control+z') + await expect(me.getByText('Diagram B')).toBeVisible() + await expect(me.getByText('Diagram A')).toBeHidden() + + // Redo should also be a no-op + await page.keyboard.press('Control+y') + await expect(me.getByText('Diagram B')).toBeVisible() + await expect(me.getByText('Diagram A')).toBeHidden() +}) + +test('clearHistory - operations after clearHistory are undoable normally', async ({ page, me }) => { + // Perform an operation in Diagram A, then switch to Diagram B and clear history + await me.click('Child A') + await page.keyboard.press('Delete') + await expect(me.getByText('Child A')).toBeHidden() + + await page.evaluate((data: typeof diagramB) => { + const mind = (window as any)['#map'] + mind.refresh(data) + mind.clearHistory() + }, diagramB) + + await expect(me.getByText('Diagram B')).toBeVisible() + + // Add a node to Diagram B + await me.click('Child B') + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + await expect(me.getByText('New Node')).toBeVisible() + + // Undo the add — should work + await page.keyboard.press('Control+z') + await expect(me.getByText('New Node')).toBeHidden() + await expect(me.getByText('Diagram B')).toBeVisible() + + // Another undo should be a no-op — must not reach Diagram A + await page.keyboard.press('Control+z') + await expect(me.getByText('Diagram B')).toBeVisible() + await expect(me.getByText('Diagram A')).toBeHidden() + + // Redo restores the added node + await page.keyboard.press('Control+y') + await expect(me.getByText('New Node')).toBeVisible() +}) + +test('clearHistory - first undo baseline is the refreshed diagram state', async ({ page, me }) => { + // Switch to Diagram B and clear history + await page.evaluate((data: typeof diagramB) => { + const mind = (window as any)['#map'] + mind.refresh(data) + mind.clearHistory() + }, diagramB) + + // Add a node to Diagram B + await me.click('Child B') + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + await expect(me.getByText('New Node')).toBeVisible() + + // Undo should restore exactly to the post-refresh state of Diagram B + await page.keyboard.press('Control+z') + await expect(me.getByText('New Node')).toBeHidden() + await expect(me.getByText('Child B')).toBeVisible() + await expect(me.getByText('Diagram B')).toBeVisible() +})