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() + }) +})