mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
fix: wire orphaned IPC channels, fix data-loss bug, print preview events, sidebar navigation
- Fix file.clearRecent sending IPC to main process instead of clearing openTabs - Wire show-batch-converter, show-document-compare, open-header-footer-dialog IPC channels - Add print preview event listeners (mc:print-preview, mc:print-preview-styled) in App.tsx - Fix sidebar hidden bridge buttons toggling sidebar closed after scroll - Update sidebar menu labels to match actual sections (Files, Outline, Git) - Fix ipc.print to be an object with show/doPrint methods (was bare function) - Update callers: ExportPdfDialog, pdf-export, PrintPreview - Add 7 regression tests for all fixes - Clean 2 obsolete snapshots - Build Linux packages (deb, AppImage, snap) — 549 tests passing
This commit is contained in:
@@ -8,7 +8,10 @@ import { useSettingsStore } from '@/stores/settings-store';
|
||||
|
||||
vi.mock('@/lib/ipc', () => ({
|
||||
ipc: {
|
||||
print: vi.fn().mockResolvedValue({ ok: true }),
|
||||
print: {
|
||||
show: vi.fn().mockResolvedValue({ ok: true }),
|
||||
doPrint: vi.fn().mockResolvedValue({ ok: true }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -29,12 +32,12 @@ describe('ExportPdfDialog', () => {
|
||||
expect(screen.getByRole('combobox', { name: /format/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('toggles ASCII tables and submits via ipc.print', async () => {
|
||||
it('toggles ASCII tables and submits via ipc.print.show', async () => {
|
||||
render(<ExportPdfDialog sourcePath="/test.md" />);
|
||||
await userEvent.click(screen.getByRole('checkbox', { name: /ascii/i }));
|
||||
await userEvent.click(screen.getByRole('button', { name: /^export$/i }));
|
||||
await waitFor(() => expect(ipc.print).toHaveBeenCalledTimes(1));
|
||||
const [arg] = (ipc.print as any).mock.calls[0];
|
||||
await waitFor(() => expect(ipc.print.show).toHaveBeenCalledTimes(1));
|
||||
const [arg] = (ipc.print.show as any).mock.calls[0];
|
||||
expect(arg.html).toContain('<!DOCTYPE html>');
|
||||
// The Markdown source is rendered to HTML inside the document body.
|
||||
expect(arg.html).toContain('<h1>hi</h1>');
|
||||
@@ -43,7 +46,7 @@ describe('ExportPdfDialog', () => {
|
||||
});
|
||||
|
||||
it('renders an error banner when IPC fails', async () => {
|
||||
(ipc.print as any).mockRejectedValueOnce(new Error('Pandoc not found'));
|
||||
(ipc.print.show as any).mockRejectedValueOnce(new Error('Pandoc not found'));
|
||||
render(<ExportPdfDialog sourcePath="/test.md" />);
|
||||
await userEvent.click(screen.getByRole('button', { name: /^export$/i }));
|
||||
expect(await screen.findByText(/pandoc not found/i)).toBeInTheDocument();
|
||||
|
||||
@@ -27,7 +27,10 @@ vi.mock('@/lib/ipc', () => ({
|
||||
pickFile: vi.fn(),
|
||||
onChange: vi.fn(),
|
||||
},
|
||||
print: vi.fn().mockResolvedValue({ ok: true }),
|
||||
print: {
|
||||
show: vi.fn().mockResolvedValue({ ok: true }),
|
||||
doPrint: vi.fn().mockResolvedValue({ ok: true }),
|
||||
},
|
||||
menu: {
|
||||
on: vi.fn(() => () => {}),
|
||||
},
|
||||
@@ -93,7 +96,7 @@ describe('Phase 8 toasts integration', () => {
|
||||
});
|
||||
|
||||
it('exporting a file calls toast.success on success', async () => {
|
||||
(ipc.print as any).mockResolvedValue({ ok: true });
|
||||
(ipc.print.show as any).mockResolvedValue({ ok: true });
|
||||
registerMenuCommands();
|
||||
render(<App />);
|
||||
|
||||
@@ -107,7 +110,7 @@ describe('Phase 8 toasts integration', () => {
|
||||
|
||||
// The PDF flow hands the rendered HTML to the main process for print.
|
||||
await waitFor(() => {
|
||||
expect(ipc.print).toHaveBeenCalledTimes(1);
|
||||
expect(ipc.print.show).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(toast.success).toHaveBeenCalledWith(expect.stringContaining('Sent test.md'));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { render, act, screen, fireEvent } from '@testing-library/react';
|
||||
import App from '@/App';
|
||||
import { useCommandStore } from '@/stores/command-store';
|
||||
import { useAppStore } from '@/stores/app-store';
|
||||
import { useFileStore } from '@/stores/file-store';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { useEditorStore } from '@/stores/editor-store';
|
||||
|
||||
vi.mock('@/lib/ipc', () => ({
|
||||
ipc: {
|
||||
file: {
|
||||
pickFolder: vi.fn(),
|
||||
pickFile: vi.fn(),
|
||||
read: vi.fn(),
|
||||
write: vi.fn(),
|
||||
list: vi.fn(),
|
||||
},
|
||||
app: { getVersion: vi.fn().mockResolvedValue({ ok: true, data: '5.0.1' }) },
|
||||
menu: { on: vi.fn(() => () => {}) },
|
||||
updater: { check: vi.fn(), install: vi.fn(), getState: vi.fn(), onStatus: vi.fn(() => () => {}) },
|
||||
crash: { read: vi.fn(), openDir: vi.fn(), delete: vi.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-welcome-trigger', () => ({
|
||||
useWelcomeTrigger: () => {},
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useAutoUpdateCheck', () => ({
|
||||
useAutoUpdateCheck: () => {},
|
||||
}));
|
||||
|
||||
describe('App — print preview event listeners', () => {
|
||||
beforeEach(() => {
|
||||
useCommandStore.setState({ handlers: {} } as any);
|
||||
useAppStore.setState({ modal: { kind: null } } as any);
|
||||
useFileStore.setState({ tree: null, rootPath: null, expanded: new Set(), openTabs: [], activeTabId: null });
|
||||
useSettingsStore.getState().resetToDefaults?.();
|
||||
useEditorStore.setState({ buffers: new Map(), activeId: null });
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('opens PrintPreview on mc:print event', () => {
|
||||
render(<App />);
|
||||
act(() => {
|
||||
window.dispatchEvent(new CustomEvent('mc:print'));
|
||||
});
|
||||
expect(screen.getByText(/print preview/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens PrintPreview on mc:print-preview event', () => {
|
||||
render(<App />);
|
||||
act(() => {
|
||||
window.dispatchEvent(new CustomEvent('mc:print-preview'));
|
||||
});
|
||||
expect(screen.getByText(/print preview/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens PrintPreview on mc:print-preview-styled event', () => {
|
||||
render(<App />);
|
||||
act(() => {
|
||||
window.dispatchEvent(new CustomEvent('mc:print-preview-styled'));
|
||||
});
|
||||
expect(screen.getByText(/print preview/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -143,4 +143,49 @@ describe('useRegisterMenuCommands + useBridgeNativeMenu', () => {
|
||||
act(() => fireMenu('load-template-menu', 'blog-post.md'));
|
||||
expect(captured).toBe('blog-post.md');
|
||||
});
|
||||
|
||||
it('file.clearRecent does NOT clear openTabs', () => {
|
||||
useFileStore.setState({
|
||||
openTabs: [
|
||||
{ id: '/a.md', path: '/a.md', title: 'a.md', dirty: false },
|
||||
],
|
||||
activeTabId: '/a.md',
|
||||
});
|
||||
render(<Harness />);
|
||||
const sendSpy = vi.fn();
|
||||
(window as any).electronAPI = { send: sendSpy };
|
||||
act(() => useCommandStore.getState().dispatch('file.clearRecent'));
|
||||
expect(useFileStore.getState().openTabs).toHaveLength(1);
|
||||
expect(sendSpy).toHaveBeenCalledWith('clear-recent-files');
|
||||
});
|
||||
|
||||
it('show-batch-converter IPC event dispatches batch.showConverter', () => {
|
||||
let captured: unknown;
|
||||
render(<Harness />);
|
||||
useCommandStore.getState().register('batch.showConverter', (args) => {
|
||||
captured = args;
|
||||
});
|
||||
act(() => fireMenu('show-batch-converter', 'image'));
|
||||
expect(captured).toBe('image');
|
||||
});
|
||||
|
||||
it('show-document-compare IPC event dispatches tools.documentCompare', () => {
|
||||
let called = false;
|
||||
render(<Harness />);
|
||||
useCommandStore.getState().register('tools.documentCompare', () => {
|
||||
called = true;
|
||||
});
|
||||
act(() => fireMenu('show-document-compare'));
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
it('open-header-footer-dialog IPC event dispatches settings.headerFooter', () => {
|
||||
let called = false;
|
||||
render(<Harness />);
|
||||
useCommandStore.getState().register('settings.headerFooter', () => {
|
||||
called = true;
|
||||
});
|
||||
act(() => fireMenu('open-header-footer-dialog'));
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user