mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-03 02:11:07 +05:30
fix(ipc): crash modal reads wrapper, search wired to real handler
CrashReportModal crashed on dumps.map because ipc.crash.read returns
{ ok, data } and the consumer was assigning the wrapper to state. Now
unwraps .data and falls back to [] on error.
ipc.file.search was miswired to ipc.file.pickFile — clicking 'Search'
popped a file picker instead of searching. Add real 'search-in-files'
handler in src/main/files/search-in-files.js with regex support,
case-sensitivity toggle, .git/node_modules/dist skip, 2MB file cap, and
1000-result limit. Wire preload bridge + TS declaration + allowlist.
Also:
- Drop dead ipc.file.open wrapper that was miswired to pickFile
- Rename FileEntry field 'modified' to 'modifiedAt' (number) so the
IPC payload matches the declared type
- Update CrashReportModal tests to use the safeCall envelope shape
This commit is contained in:
@@ -9,35 +9,57 @@ vi.mock('@/lib/ipc', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* The crash:read IPC handler returns a plain array; ipc.crash.read wraps it
|
||||
* in { ok: true, data }. The component must unwrap .data before rendering
|
||||
* — otherwise dumps.length is undefined and dumps.map blows up.
|
||||
*/
|
||||
function wrap(data: unknown) {
|
||||
return { ok: true, data };
|
||||
}
|
||||
|
||||
describe('CrashReportModal', () => {
|
||||
test('shows empty state when no crashes', async () => {
|
||||
(ipc.crash.read as any).mockResolvedValue([]);
|
||||
(ipc.crash.read as any).mockResolvedValue(wrap([]));
|
||||
render(<CrashReportModal onClose={() => {}} />);
|
||||
expect(await screen.findByText(/no crashes/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows empty state when ipc returns ok:false', async () => {
|
||||
(ipc.crash.read as any).mockResolvedValue({
|
||||
ok: false,
|
||||
error: { code: 'NO_BRIDGE', message: 'no electronAPI' },
|
||||
});
|
||||
render(<CrashReportModal onClose={() => {}} />);
|
||||
expect(await screen.findByText(/no crashes/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('lists crashes returned by ipc', async () => {
|
||||
(ipc.crash.read as any).mockResolvedValue([
|
||||
{
|
||||
filename: '1700000000000-1-uncaughtException.json',
|
||||
kind: 'uncaughtException',
|
||||
message: 'boom',
|
||||
timestamp: '2026-01-01T00:00:00.000Z',
|
||||
},
|
||||
]);
|
||||
(ipc.crash.read as any).mockResolvedValue(
|
||||
wrap([
|
||||
{
|
||||
filename: '1700000000000-1-uncaughtException.json',
|
||||
kind: 'uncaughtException',
|
||||
message: 'boom',
|
||||
timestamp: '2026-01-01T00:00:00.000Z',
|
||||
},
|
||||
])
|
||||
);
|
||||
render(<CrashReportModal onClose={() => {}} />);
|
||||
expect(await screen.findByText(/boom/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('delete button calls ipc.crash.delete', async () => {
|
||||
(ipc.crash.read as any).mockResolvedValue([
|
||||
{
|
||||
filename: '1700000000000-1-uncaughtException.json',
|
||||
kind: 'uncaughtException',
|
||||
message: 'boom',
|
||||
timestamp: '2026-01-01T00:00:00.000Z',
|
||||
},
|
||||
]);
|
||||
(ipc.crash.read as any).mockResolvedValue(
|
||||
wrap([
|
||||
{
|
||||
filename: '1700000000000-1-uncaughtException.json',
|
||||
kind: 'uncaughtException',
|
||||
message: 'boom',
|
||||
timestamp: '2026-01-01T00:00:00.000Z',
|
||||
},
|
||||
])
|
||||
);
|
||||
render(<CrashReportModal onClose={() => {}} />);
|
||||
const btn = await screen.findByText(/delete/i);
|
||||
fireEvent.click(btn);
|
||||
@@ -47,7 +69,7 @@ describe('CrashReportModal', () => {
|
||||
});
|
||||
|
||||
test('open folder button calls ipc.crash.openDir', async () => {
|
||||
(ipc.crash.read as any).mockResolvedValue([]);
|
||||
(ipc.crash.read as any).mockResolvedValue(wrap([]));
|
||||
render(<CrashReportModal onClose={() => {}} />);
|
||||
fireEvent.click(await screen.findByText(/open dump folder/i));
|
||||
expect(ipc.crash.openDir).toHaveBeenCalled();
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const { searchInFiles } = require('../../../../src/main/files/search-in-files');
|
||||
|
||||
describe('searchInFiles', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'search-in-files-'));
|
||||
});
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('returns empty array when rootPath or query missing', () => {
|
||||
expect(searchInFiles({ rootPath: '', query: 'x' })).toEqual([]);
|
||||
expect(searchInFiles({ rootPath: tmpDir, query: '' })).toEqual([]);
|
||||
});
|
||||
|
||||
test('finds a literal substring in a single file', () => {
|
||||
const f = path.join(tmpDir, 'a.md');
|
||||
fs.writeFileSync(f, 'hello world\nsecond line\nhello again');
|
||||
const out = searchInFiles({ rootPath: tmpDir, query: 'hello' });
|
||||
expect(out).toEqual([
|
||||
{ filePath: f, line: 1, content: 'hello world' },
|
||||
{ filePath: f, line: 3, content: 'hello again' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('respects caseSensitive flag (default false)', () => {
|
||||
const f = path.join(tmpDir, 'a.md');
|
||||
fs.writeFileSync(f, 'Foo bar\nfoo BAR');
|
||||
expect(searchInFiles({ rootPath: tmpDir, query: 'foo' }).length).toBe(2);
|
||||
expect(searchInFiles({ rootPath: tmpDir, query: 'foo', caseSensitive: true }).length).toBe(1);
|
||||
});
|
||||
|
||||
test('supports regex when isRegex is true', () => {
|
||||
const f = path.join(tmpDir, 'a.md');
|
||||
fs.writeFileSync(f, 'foo123\nbar\nbaz456');
|
||||
const out = searchInFiles({ rootPath: tmpDir, query: '\\w+\\d+', isRegex: true });
|
||||
expect(out.map((r) => r.content)).toEqual(['foo123', 'baz456']);
|
||||
});
|
||||
|
||||
test('invalid regex returns empty array (does not throw)', () => {
|
||||
expect(searchInFiles({ rootPath: tmpDir, query: '[unclosed', isRegex: true })).toEqual([]);
|
||||
});
|
||||
|
||||
test('skips node_modules, .git, dist directories', () => {
|
||||
const skip = path.join(tmpDir, 'node_modules');
|
||||
fs.mkdirSync(skip);
|
||||
fs.writeFileSync(path.join(skip, 'a.md'), 'match me');
|
||||
const keep = path.join(tmpDir, 'src');
|
||||
fs.mkdirSync(keep);
|
||||
fs.writeFileSync(path.join(keep, 'b.md'), 'match me');
|
||||
const out = searchInFiles({ rootPath: tmpDir, query: 'match' });
|
||||
expect(out.length).toBe(1);
|
||||
expect(out[0].filePath).toContain('src/b.md');
|
||||
});
|
||||
|
||||
test('skips files larger than 2MB', () => {
|
||||
const big = path.join(tmpDir, 'big.md');
|
||||
fs.writeFileSync(big, 'a'.repeat(3 * 1024 * 1024));
|
||||
expect(searchInFiles({ rootPath: tmpDir, query: 'a' })).toEqual([]);
|
||||
});
|
||||
|
||||
test('recurses into subdirectories', () => {
|
||||
const sub = path.join(tmpDir, 'a', 'b', 'c');
|
||||
fs.mkdirSync(sub, { recursive: true });
|
||||
const f = path.join(sub, 'deep.md');
|
||||
fs.writeFileSync(f, 'needle here');
|
||||
const out = searchInFiles({ rootPath: tmpDir, query: 'needle' });
|
||||
expect(out.length).toBe(1);
|
||||
expect(out[0].filePath).toBe(f);
|
||||
expect(out[0].line).toBe(1);
|
||||
});
|
||||
|
||||
test('caps results at 1000', () => {
|
||||
const f = path.join(tmpDir, 'many.md');
|
||||
const lines = [];
|
||||
for (let i = 0; i < 1500; i++) lines.push('match line');
|
||||
fs.writeFileSync(f, lines.join('\n'));
|
||||
const out = searchInFiles({ rootPath: tmpDir, query: 'match' });
|
||||
expect(out.length).toBe(1000);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user