diff --git a/frontend/src/components/panels/Sidebar.tsx b/frontend/src/components/panels/Sidebar.tsx
index 1155f7f..ed195b9 100644
--- a/frontend/src/components/panels/Sidebar.tsx
+++ b/frontend/src/components/panels/Sidebar.tsx
@@ -257,7 +257,7 @@ export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbee
icon={Save}
label="Save Canvas"
collapsed={collapsed}
- onClick={onSave}
+ onClick={() => onSave()}
badge={hasUnsavedChanges}
accent
/>
diff --git a/frontend/src/components/panels/Toolbar.tsx b/frontend/src/components/panels/Toolbar.tsx
index a72567a..87d1d72 100644
--- a/frontend/src/components/panels/Toolbar.tsx
+++ b/frontend/src/components/panels/Toolbar.tsx
@@ -95,7 +95,7 @@ export function Toolbar({ onSave, onAutoLayout, onExport, onChangeStyle, onUndo,
background: hasUnsavedChanges ? '#00d4ff' : undefined,
color: hasUnsavedChanges ? '#0d1117' : undefined,
}}
- onClick={onSave}
+ onClick={() => onSave()}
>
{hasUnsavedChanges && (
diff --git a/frontend/src/components/panels/__tests__/Sidebar.test.tsx b/frontend/src/components/panels/__tests__/Sidebar.test.tsx
index 0e97a93..1ee0da8 100644
--- a/frontend/src/components/panels/__tests__/Sidebar.test.tsx
+++ b/frontend/src/components/panels/__tests__/Sidebar.test.tsx
@@ -188,6 +188,15 @@ describe('Sidebar', () => {
expect(defaultProps.onSave).toHaveBeenCalledOnce()
})
+ // Regression (#186): the click handler must not forward the MouseEvent as an
+ // argument — handleSave treats its first arg as a designIdOverride, so leaking
+ // the event corrupts design_id and the save silently fails.
+ it('calls onSave with no arguments (does not leak the click event)', () => {
+ render()
+ fireEvent.click(screen.getByText('Save Canvas'))
+ expect(defaultProps.onSave).toHaveBeenCalledWith()
+ })
+
it('calls onOpenSettings when Settings is clicked', () => {
render()
fireEvent.click(screen.getByText('Settings'))
diff --git a/frontend/src/components/panels/__tests__/Toolbar.test.tsx b/frontend/src/components/panels/__tests__/Toolbar.test.tsx
new file mode 100644
index 0000000..0540929
--- /dev/null
+++ b/frontend/src/components/panels/__tests__/Toolbar.test.tsx
@@ -0,0 +1,59 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
+import { Toolbar } from '../Toolbar'
+import { useCanvasStore } from '@/stores/canvasStore'
+
+// ── Mocks ────────────────────────────────────────────────────────────────────
+
+vi.mock('@/stores/canvasStore')
+
+vi.mock('@/components/ui/Logo', () => ({
+ Logo: () =>
,
+}))
+
+function mockStore(overrides: Partial> = {}) {
+ vi.mocked(useCanvasStore).mockReturnValue({
+ hasUnsavedChanges: false,
+ past: [],
+ future: [],
+ ...overrides,
+ } as ReturnType)
+}
+
+const defaultProps = {
+ onSave: vi.fn(),
+ onAutoLayout: vi.fn(),
+ onExport: vi.fn(),
+ onChangeStyle: vi.fn(),
+ onUndo: vi.fn(),
+ onRedo: vi.fn(),
+ onShortcuts: vi.fn(),
+ onExportMd: vi.fn(),
+ onExportYaml: vi.fn(),
+ onImportYaml: vi.fn(),
+ onViewOnly: vi.fn(),
+}
+
+// ── Tests ─────────────────────────────────────────────────────────────────────
+
+describe('Toolbar', () => {
+ beforeEach(() => {
+ mockStore()
+ vi.clearAllMocks()
+ })
+
+ it('calls onSave when Save is clicked', () => {
+ render()
+ fireEvent.click(screen.getByText('Save'))
+ expect(defaultProps.onSave).toHaveBeenCalledOnce()
+ })
+
+ // Regression (#186): the click handler must not forward the MouseEvent as an
+ // argument — handleSave treats its first arg as a designIdOverride, so leaking
+ // the event corrupts design_id and the save silently fails.
+ it('calls onSave with no arguments (does not leak the click event)', () => {
+ render()
+ fireEvent.click(screen.getByText('Save'))
+ expect(defaultProps.onSave).toHaveBeenCalledWith()
+ })
+})