mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
The v5.0.0 React UI shipped with a working surface but had a number of
features left as 'wired in a later phase' or broken by missing IPC
plumbing. This commit completes the wiring end-to-end and proves the
behaviour with the run-desktop driver.
Toolbar format buttons (Toolbar.tsx)
- Bold, Italic, Unordered/Ordered list, Code, Link now dispatch real
commands instead of being permanently disabled.
New module: src/renderer/lib/editor-commands.ts
- Module-level singleton holding the active CodeMirror EditorView.
- toggleBold/Italic/Code/CodeBlock, toggleUnorderedList/OrderedList,
insertLink, setHeadingLevel, scrollToLine, undo, redo, insertSnippet.
- CodeMirrorEditor mounts the view via setActiveView on mount and
nulls it on unmount, so commands land on the focused buffer.
register-menu-commands.ts
- file.confirmClose: prompts before closing a dirty tab via the
confirm modal; otherwise closes silently.
- app.quit: prompts before quitting if any buffer is dirty; uses
the new ipc.app.quit channel.
- short-cuts.show: shows the keyboard shortcut list in a confirm dialog.
- editor.bold/italic/code/list.*/link/undo/redo/heading.*: drive the
active editor.
- find.toggle: dispatches mc:find-toggle for any listening component.
- view.sidebarPanel/bottomPanel: navigate between sidebar sections
via the new data-testid buttons; bottom panel toggles the REPL.
- font.size: increase/decrease/reset on editorFontSize (10..28).
- theme.loadCustomCss/clearCustomCss: pick and clear the custom CSS
path on the settings store.
- template.load: inserts a bundled markdown skeleton at the cursor.
- print.preview/previewStyled: emit mc:print and mc:print-preview.
- file.clearRecent: clears open tabs.
- file.new: creates an untitled buffer and tab.
- editor.gotoHeading: scrolls to a given line (used by Outline and
Breadcrumb).
- view.toggleSidebar: already wired.
- file.opened: already wired (prior fix).
Sidebar Outline + Breadcrumb
- Both now click through to 'editor.gotoHeading' with the line
number extracted from the active buffer.
- Sidebar exposes data-testid for the 'view.sidebarPanel' menu action.
editor-commands sets the active view; scroll metrics flow through to
Minimap which now reads scrollRatio/visibleRatio from the editor.
Export dialogs (PDF/DOCX/HTML)
- Replaced the broken ipc.export.{pdf,docx,html,batch} calls (those
channels were not implemented in main and only CHANNEL_MISSING
was returned) with renderer-side pipelines.
- PDF: generateHtml() with @page CSS for size + margins, then
ipc.print to the main process for native print-to-PDF.
- DOCX: generateDocx() from the existing lib, then ipc.file.writeBuffer.
- HTML: generateHtml() then ipc.file.writeBuffer.
- All three dialogs use ipc.app.showSaveDialog for output path
(new IPC handler) and ipc.file.writeBuffer for the binary write
(new preload channel).
- PrintPreview now uses MarkdownRenderer instead of a raw <pre>.
New settings fields
- editorFontSize: 10..28 px (drives CodeMirror theme font-size)
- customCssPath: string|null (Theme menu wires 'load-custom-css' and
'clear-custom-css' to it).
New IPC channels
- 'app:quit', 'app:open-external', 'app:show-save-dialog',
'write-buffer' (already existed; now exposed in preload).
Tests
- Updated Toolbar test: format buttons are no longer disabled and
dispatch their command ids.
- Updated Export*D*Dialog tests to mock the new ipc surface
(ipc.print, ipc.app.showSaveDialog, ipc.file.writeBuffer) and
assert the new flow.
- Updated PrintPreview test to use ipc.print.doPrint.
- Updated phase8-toasts integration: PDF flow now goes through
ipc.print and asserts 'Sent <title> to printer'.
- register-menu-commands test: re-register 'template.load' AFTER
Harness renders so the captured-args handler is the live one.
Verification
- npm run build:renderer: success (1.6s)
- npx vitest run: 308 passed (up from 305; 3 new + unchanged)
- npx jest: 189 passed (unchanged)
- .claude/skills/run-desktop/verify-features.mjs: drove the live app
via Playwright, exercised every wired feature end-to-end, captured
11 screenshots. All verified working (editor + preview render the
file content, toolbar buttons dispatch, dialogs open, theme
toggles, outline shows headings, italic button works in editor).
62 lines
2.8 KiB
TypeScript
62 lines
2.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { ExportDocxDialog } from '@/components/modals/ExportDocxDialog';
|
|
import { useFileStore } from '@/stores/file-store';
|
|
import { useEditorStore } from '@/stores/editor-store';
|
|
import { useSettingsStore } from '@/stores/settings-store';
|
|
|
|
vi.mock('@/lib/ipc', () => ({
|
|
ipc: {
|
|
app: { showSaveDialog: vi.fn() },
|
|
file: { writeBuffer: vi.fn() },
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/lib/docx-export', () => ({
|
|
generateDocx: vi.fn().mockResolvedValue(new Blob([new Uint8Array(8)], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' })),
|
|
}));
|
|
|
|
import { ipc } from '@/lib/ipc';
|
|
|
|
describe('ExportDocxDialog', () => {
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
vi.clearAllMocks();
|
|
// The dialog passes the source path with .docx extension as the default
|
|
// path, and the test mock returns that same path (echoing the default).
|
|
(ipc.app.showSaveDialog as any).mockImplementation(async (args) => ({
|
|
ok: true,
|
|
data: args?.defaultPath ?? '/out.docx',
|
|
}));
|
|
(ipc.file.writeBuffer as any).mockResolvedValue({ ok: true });
|
|
useSettingsStore.setState(useSettingsStore.getInitialState());
|
|
useFileStore.setState({ activeTabId: '/test.md', openTabs: [{ id: '/test.md', path: '/test.md', title: 'test.md', dirty: false }] } as any);
|
|
useEditorStore.setState({ buffers: new Map([['/test.md', { id: '/test.md', path: '/test.md', content: '# hi', dirty: false }]]) } as any);
|
|
});
|
|
|
|
it('renders with standard template selected by default', () => {
|
|
render(<ExportDocxDialog sourcePath="/test.md" />);
|
|
expect(screen.getByText(/export to docx/i)).toBeInTheDocument();
|
|
expect(screen.getByRole('combobox', { name: /template/i })).toBeInTheDocument();
|
|
});
|
|
|
|
it('submitting with default options writes a docx buffer', async () => {
|
|
render(<ExportDocxDialog sourcePath="/test.md" />);
|
|
await userEvent.click(screen.getByRole('button', { name: /^export$/i }));
|
|
await waitFor(() => expect(ipc.app.showSaveDialog).toHaveBeenCalledTimes(1));
|
|
expect(ipc.file.writeBuffer).toHaveBeenCalledTimes(1);
|
|
const callArg = (ipc.file.writeBuffer as any).mock.calls[0][0];
|
|
expect(callArg.path).toBe('/test.docx');
|
|
expect(callArg.buffer).toBeDefined();
|
|
expect(callArg.buffer.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('selecting "modern" template is reflected in the rendered form', async () => {
|
|
render(<ExportDocxDialog sourcePath="/test.md" />);
|
|
await userEvent.click(screen.getByRole('combobox', { name: /template/i }));
|
|
await userEvent.click(screen.getByRole('option', { name: /modern/i }));
|
|
expect(screen.getByRole('combobox', { name: /template/i })).toHaveTextContent(/modern/i);
|
|
});
|
|
});
|