mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
feat: complete master feature parity with interactive PDF editing, Reveal.js export dialog, Large File Mode, and scoped CSS
This commit is contained in:
@@ -7,10 +7,8 @@ import { useAppStore } from '@/stores/app-store';
|
||||
describe('AboutDialog', () => {
|
||||
beforeEach(() => {
|
||||
window.electronAPI = {
|
||||
app: {
|
||||
getVersion: vi.fn().mockResolvedValue('5.0.0'),
|
||||
openExternal: vi.fn().mockResolvedValue({ ok: true }),
|
||||
},
|
||||
getAppVersion: vi.fn().mockResolvedValue('5.0.0'),
|
||||
send: vi.fn().mockResolvedValue({ ok: true }),
|
||||
} as any;
|
||||
// Reset store to about modal so dialog renders
|
||||
useAppStore.setState({ modal: { kind: 'about' } } as any);
|
||||
|
||||
@@ -7,12 +7,12 @@ describe('ExportBatchDialog', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
window.electronAPI = {
|
||||
export: {
|
||||
batch: vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
data: { total: 2, succeeded: 2, failed: 0, results: [] },
|
||||
}),
|
||||
},
|
||||
invoke: vi.fn().mockResolvedValue({
|
||||
total: 2,
|
||||
succeeded: 2,
|
||||
failed: 0,
|
||||
results: [],
|
||||
}),
|
||||
} as any;
|
||||
});
|
||||
|
||||
@@ -27,11 +27,12 @@ describe('ExportBatchDialog', () => {
|
||||
await userEvent.click(screen.getByRole('combobox', { name: /format/i }));
|
||||
await userEvent.click(screen.getByRole('option', { name: /^pdf$/i }));
|
||||
await userEvent.click(screen.getByRole('button', { name: /^export$/i }));
|
||||
const call = (window.electronAPI.export.batch as any).mock.calls[0];
|
||||
expect(call[0]).toEqual([
|
||||
const call = (window.electronAPI.invoke as any).mock.calls[0];
|
||||
expect(call[0]).toBe('batch-convert');
|
||||
expect(call[1].items).toEqual([
|
||||
{ inputPath: '/a.md', outputPath: expect.any(String) },
|
||||
{ inputPath: '/b.md', outputPath: expect.any(String) },
|
||||
]);
|
||||
expect(call[1].format).toBe('pdf');
|
||||
expect(call[1].options.format).toBe('pdf');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,28 +18,45 @@ const defaultSettings = {
|
||||
logoPath: null,
|
||||
};
|
||||
|
||||
const mockGetSettings = vi.fn().mockResolvedValue(defaultSettings);
|
||||
const mockSaveSettings = vi.fn().mockResolvedValue(undefined);
|
||||
const mockBrowseLogo = vi.fn().mockResolvedValue('/path/to/logo.png');
|
||||
let registeredCallback: ((data: any) => void) | null = null;
|
||||
const mockSend = vi.fn((channel, ...args) => {
|
||||
if (channel === 'get-header-footer-settings') {
|
||||
setTimeout(() => {
|
||||
if (registeredCallback) registeredCallback(defaultSettings);
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
const mockOn = vi.fn((channel, cb) => {
|
||||
if (channel === 'header-footer-settings-data') {
|
||||
registeredCallback = cb as any;
|
||||
// Trigger immediately with default settings to satisfy initial mount loading
|
||||
setTimeout(() => {
|
||||
cb(defaultSettings);
|
||||
}, 0);
|
||||
}
|
||||
return vi.fn(); // cleanup/unsubscribe fn
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
registeredCallback = null;
|
||||
useSettingsStore.setState(useSettingsStore.getInitialState());
|
||||
useAppStore.setState({ modal: { kind: null } } as any);
|
||||
(window.electronAPI as any) = {
|
||||
headerFooter: {
|
||||
getSettings: mockGetSettings,
|
||||
saveSettings: mockSaveSettings,
|
||||
browseLogo: mockBrowseLogo,
|
||||
},
|
||||
send: mockSend,
|
||||
on: mockOn,
|
||||
once: vi.fn(),
|
||||
invoke: vi.fn(() => Promise.resolve(null)),
|
||||
removeAllListeners: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('HeaderFooterDialog', () => {
|
||||
it('loads settings from electronAPI on mount', async () => {
|
||||
render(<HeaderFooterDialog />);
|
||||
await waitFor(() => expect(mockGetSettings).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mockSend).toHaveBeenCalledWith('get-header-footer-settings'));
|
||||
expect(screen.getByText(/header & footer/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -61,7 +78,7 @@ describe('HeaderFooterDialog', () => {
|
||||
render(<HeaderFooterDialog />);
|
||||
await waitFor(() => expect(screen.getByText(/header & footer/i)).toBeInTheDocument());
|
||||
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
||||
await waitFor(() => expect(mockSaveSettings).toHaveBeenCalled());
|
||||
await waitFor(() => expect(mockSend).toHaveBeenCalledWith('save-header-footer-settings', expect.any(Object)));
|
||||
});
|
||||
|
||||
it('closes modal on Cancel', async () => {
|
||||
@@ -72,7 +89,16 @@ describe('HeaderFooterDialog', () => {
|
||||
});
|
||||
|
||||
it('inserts dynamic field token into header input', async () => {
|
||||
mockGetSettings.mockResolvedValue({ ...defaultSettings, headerEnabled: true });
|
||||
// Override registered callback to return enabled header
|
||||
mockOn.mockImplementationOnce((channel, cb) => {
|
||||
if (channel === 'header-footer-settings-data') {
|
||||
setTimeout(() => {
|
||||
cb({ ...defaultSettings, headerEnabled: true });
|
||||
}, 0);
|
||||
}
|
||||
return vi.fn();
|
||||
});
|
||||
|
||||
render(<HeaderFooterDialog />);
|
||||
await waitFor(() => expect(screen.getByText(/header & footer/i)).toBeInTheDocument());
|
||||
const tokenButton = screen.getAllByTitle('Page')[0];
|
||||
|
||||
@@ -7,6 +7,28 @@ const mockProcessOperation = vi.fn().mockResolvedValue(undefined);
|
||||
const mockPickFile = vi.fn();
|
||||
const mockPickFolder = vi.fn();
|
||||
const mockShowSaveDialog = vi.fn();
|
||||
const mockReadBuffer = vi.fn();
|
||||
|
||||
// Mock pdfjs-dist to avoid rendering/worker issues in node test environment
|
||||
vi.mock('pdfjs-dist', () => {
|
||||
const mockRenderPromise = Promise.resolve();
|
||||
const mockPage = {
|
||||
getViewport: vi.fn().mockReturnValue({ width: 100, height: 150 }),
|
||||
render: vi.fn().mockReturnValue({ promise: mockRenderPromise }),
|
||||
};
|
||||
const mockDoc = {
|
||||
numPages: 3,
|
||||
getPage: vi.fn().mockResolvedValue(mockPage),
|
||||
};
|
||||
return {
|
||||
GlobalWorkerOptions: {
|
||||
workerSrc: '',
|
||||
},
|
||||
getDocument: vi.fn().mockReturnValue({
|
||||
promise: Promise.resolve(mockDoc),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
@@ -14,10 +36,12 @@ beforeEach(() => {
|
||||
mockPickFile.mockResolvedValue({ ok: true, data: null });
|
||||
mockPickFolder.mockResolvedValue({ ok: true, data: null });
|
||||
mockShowSaveDialog.mockResolvedValue({ ok: true, data: null });
|
||||
mockReadBuffer.mockResolvedValue({ ok: true, data: new Uint8Array([1, 2, 3]) });
|
||||
window.electronAPI = {
|
||||
file: {
|
||||
pickFile: mockPickFile,
|
||||
pickFolder: mockPickFolder,
|
||||
readBuffer: mockReadBuffer,
|
||||
},
|
||||
app: {
|
||||
showSaveDialog: mockShowSaveDialog,
|
||||
@@ -294,4 +318,69 @@ describe('PdfEditorDialog', () => {
|
||||
await userEvent.click(screen.getByText('Compress'));
|
||||
expect(screen.queryByText(/add at least 2 pdf files/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders interactive page thumbnails in rotate tab', async () => {
|
||||
render(<PdfEditorDialog onClose={() => {}} initialFilePath="/test.pdf" />);
|
||||
|
||||
// Switch to Rotate tab
|
||||
await userEvent.click(screen.getByText('Rotate'));
|
||||
|
||||
// Wait for the thumbnails to load and render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Page 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Page 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Page 3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Check Rotate button on page 1 thumbnail
|
||||
const rotateBtns = screen.getAllByTitle('Rotate 90°');
|
||||
expect(rotateBtns).toHaveLength(3);
|
||||
|
||||
// Click rotate on page 1
|
||||
await userEvent.click(rotateBtns[0]);
|
||||
|
||||
// Check if the form's "Pages" input is updated to "1"
|
||||
const pagesInput = screen.getByPlaceholderText('All pages') as HTMLInputElement;
|
||||
expect(pagesInput.value).toBe('1');
|
||||
});
|
||||
|
||||
it('toggles page deletion status in delete tab', async () => {
|
||||
render(<PdfEditorDialog onClose={() => {}} initialFilePath="/test.pdf" />);
|
||||
|
||||
// Switch to Delete tab
|
||||
await userEvent.click(screen.getByText('Delete'));
|
||||
|
||||
// Wait for the thumbnails
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Page 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click Delete button on page 1
|
||||
const deleteBtns = screen.getAllByTitle('Delete');
|
||||
await userEvent.click(deleteBtns[0]);
|
||||
|
||||
// Check if the pages to delete input is updated to "1"
|
||||
const pagesInput = screen.getByPlaceholderText('1,3,5-8') as HTMLInputElement;
|
||||
expect(pagesInput.value).toBe('1');
|
||||
});
|
||||
|
||||
it('reorders pages in reorder tab', async () => {
|
||||
render(<PdfEditorDialog onClose={() => {}} initialFilePath="/test.pdf" />);
|
||||
|
||||
// Switch to Reorder tab
|
||||
await userEvent.click(screen.getByText('Reorder'));
|
||||
|
||||
// Wait for the thumbnails
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Page 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Move Page 1 Down
|
||||
const moveDownBtns = screen.getAllByTitle('Move Down');
|
||||
await userEvent.click(moveDownBtns[0]);
|
||||
|
||||
// Check if the new order input is updated
|
||||
const newOrderInput = screen.getByPlaceholderText('3,1,2,5,4') as HTMLTextAreaElement;
|
||||
expect(newOrderInput.value).toBe('2,1,3');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user