Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/plugin/operationHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ export default function (mei: MindElixirInstance) {
}
}
}
mei.clearHistory = function () {
history = []
currentIndex = -1
current = mei.getData()
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clearHistory() clears the history stack but leaves currentSelectedNodes unchanged. Since the intended usage is right after refresh(), selection is typically cleared, and the next operation may incorrectly record the previous diagram’s selected node IDs in currentSelected, which can break selection restoration on undo/redo. Reset currentSelectedNodes inside clearHistory() (e.g., derive it from mei.currentNodes) or update the selection tracking to handle deselection events as well.

Suggested change
current = mei.getData()
current = mei.getData()
// keep cached selection in sync with current diagram state
currentSelectedNodes = mei.currentNodes.map(n => n.nodeObj)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 601dc4b — added currentSelectedNodes = [] to clearHistory() so stale node IDs from the previous diagram cannot affect selection restoration after refresh() + clearHistory().

mei.clearSelection()
}
Comment on lines +91 to +96
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No automated test is added for the new clearHistory() behavior. There are existing Playwright specs covering operation history/undo/redo; please add a test that (1) performs some operations, (2) calls refresh(newData) + clearHistory(), then (3) asserts that undo/redo cannot revert into the pre-refresh diagram and that the first undo baseline is the refreshed data.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 1e50bf0 — new file tests/clear-history.spec.ts with three Playwright tests:

  1. Undo cannot revert into the pre-refresh diagram — performs ops on diagram A, calls refresh(B) + clearHistory(), then asserts Ctrl+Z/Ctrl+Y are no-ops across the diagram boundary.
  2. Operations after clearHistory() are undoable normally — verifies that new operations on diagram B undo/redo correctly, and that the stack doesn't reach back into diagram A.
  3. First undo baseline is the refreshed state — verifies that undoing the first post-refresh operation lands exactly on the refreshed diagram, not an earlier one.

const handleOperation = function (operation: Operation) {
if (operation.name === 'beginEdit') return
history = history.slice(0, currentIndex + 1)
Expand Down
9 changes: 9 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ export interface MindElixirInstance extends Omit<Required<Options>, '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<typeof createDragMoveHelper>
Expand Down
105 changes: 105 additions & 0 deletions tests/clear-history.spec.ts
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the test is failing, please remove it for now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oups...
The tests should pass now.

Original file line number Diff line number Diff line change
@@ -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()
})