mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 10:00:17 +05:30
fix: list-directory returns flat array, drop defensive fallback
The list-directory handler returned { path, entries } while the
renderer typed result.data as FileEntry[]; the file-store had to fall
back to raw.entries via Array.isArray. Extract the entries builder into
src/main/files/list-directory.js so it can be unit-tested, return a
plain array, and skip entries that can't be stat'd (broken symlinks,
permission errors) instead of throwing.
Also tighten jest config so dist/*.snap electron-builder artifacts are
not matched as test suites.
This commit is contained in:
+16
-3
@@ -10,10 +10,23 @@ module.exports = {
|
|||||||
// Root directory
|
// Root directory
|
||||||
rootDir: '.',
|
rootDir: '.',
|
||||||
|
|
||||||
// Test file patterns
|
// Test file patterns — scoped to ./tests/ only so dist/ artifacts like
|
||||||
|
// .snap packages don't get matched as test suites.
|
||||||
testMatch: [
|
testMatch: [
|
||||||
'**/tests/**/*.test.js',
|
'<rootDir>/tests/**/*.test.js',
|
||||||
'**/tests/**/*.spec.js'
|
'<rootDir>/tests/**/*.spec.js'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Ignore build outputs so .snap packages and bundled .asar contents
|
||||||
|
// never enter jest's file discovery.
|
||||||
|
testPathIgnorePatterns: [
|
||||||
|
'/node_modules/',
|
||||||
|
'/dist/',
|
||||||
|
'/\\.git/'
|
||||||
|
],
|
||||||
|
modulePathIgnorePatterns: [
|
||||||
|
'/node_modules/',
|
||||||
|
'/dist/'
|
||||||
],
|
],
|
||||||
|
|
||||||
// Coverage configuration
|
// Coverage configuration
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List a directory's entries (excluding dotfiles), sorted directories-first
|
||||||
|
* then alphabetically. Returns a flat array of FileEntry-shaped objects,
|
||||||
|
* matching the renderer type declaration in `src/renderer/lib/ipc.ts`.
|
||||||
|
*
|
||||||
|
* Skips entries that cannot be stat'd (permission errors, broken symlinks)
|
||||||
|
* instead of throwing — keeps the UI responsive on partially-readable dirs.
|
||||||
|
*/
|
||||||
|
function listDirectoryEntries(dirPath) {
|
||||||
|
const dirents = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
const entries = [];
|
||||||
|
for (const d of dirents) {
|
||||||
|
if (d.name.startsWith('.')) continue;
|
||||||
|
const full = path.join(dirPath, d.name);
|
||||||
|
let size = 0;
|
||||||
|
let modified = 0;
|
||||||
|
try {
|
||||||
|
const s = fs.statSync(full);
|
||||||
|
size = d.isDirectory() ? 0 : s.size;
|
||||||
|
modified = s.mtimeMs;
|
||||||
|
} catch (_err) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entries.push({
|
||||||
|
name: d.name,
|
||||||
|
isDirectory: d.isDirectory(),
|
||||||
|
size,
|
||||||
|
modified,
|
||||||
|
path: full,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
entries.sort((a, b) => {
|
||||||
|
if (a.isDirectory && !b.isDirectory) return -1;
|
||||||
|
if (!a.isDirectory && b.isDirectory) return 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { listDirectoryEntries };
|
||||||
+2
-16
@@ -13,6 +13,7 @@ const WordTemplateExporter = require('./word-template');
|
|||||||
const PDFOperations = require('./PDFOperations');
|
const PDFOperations = require('./PDFOperations');
|
||||||
const { validatePath, resolveWritablePath, isPathAccessible } = require('./utils/paths');
|
const { validatePath, resolveWritablePath, isPathAccessible } = require('./utils/paths');
|
||||||
const fileOps = require('./files');
|
const fileOps = require('./files');
|
||||||
|
const { listDirectoryEntries } = require('./files/list-directory');
|
||||||
const menu = require('./menu');
|
const menu = require('./menu');
|
||||||
const { createMainWindow } = require('./window');
|
const { createMainWindow } = require('./window');
|
||||||
const MonospaceFontConfig = require('./MonospaceFontConfig');
|
const MonospaceFontConfig = require('./MonospaceFontConfig');
|
||||||
@@ -3522,22 +3523,7 @@ ipcMain.handle('list-directory', async (event, dirPath) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = fs
|
return listDirectoryEntries(validation.resolved);
|
||||||
.readdirSync(validation.resolved, { withFileTypes: true })
|
|
||||||
.filter((e) => !e.name.startsWith('.'))
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
||||||
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
})
|
|
||||||
.map((e) => ({
|
|
||||||
name: e.name,
|
|
||||||
isDirectory: e.isDirectory(),
|
|
||||||
size: e.isDirectory() ? 0 : fs.statSync(path.join(validation.resolved, e.name)).size,
|
|
||||||
modified: fs.statSync(path.join(validation.resolved, e.name)).mtimeMs,
|
|
||||||
path: path.join(validation.resolved, e.name),
|
|
||||||
}));
|
|
||||||
return { path: validation.resolved, entries };
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('list-directory error:', err);
|
console.error('list-directory error:', err);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -86,8 +86,7 @@ export const useFileStore = create<FileState>()(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const raw: any = result.data!;
|
const entries = result.data ?? [];
|
||||||
const entries: any[] = Array.isArray(raw) ? raw : raw.entries;
|
|
||||||
const children: FileNode[] = entries.map(entryToNode);
|
const children: FileNode[] = entries.map(entryToNode);
|
||||||
|
|
||||||
set((s) => {
|
set((s) => {
|
||||||
@@ -119,8 +118,7 @@ export const useFileStore = create<FileState>()(
|
|||||||
|
|
||||||
set((s) => {
|
set((s) => {
|
||||||
updateNode(s.tree!, dirPath, (node) => {
|
updateNode(s.tree!, dirPath, (node) => {
|
||||||
const raw: any = result.data!;
|
const items = result.data ?? [];
|
||||||
const items: any[] = Array.isArray(raw) ? raw : raw.entries;
|
|
||||||
node.children = items.map(entryToNode);
|
node.children = items.map(entryToNode);
|
||||||
node.loaded = true;
|
node.loaded = true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
const { listDirectoryEntries } = require('../../../../src/main/files/list-directory');
|
||||||
|
|
||||||
|
describe('listDirectoryEntries', () => {
|
||||||
|
let tmpDir;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'list-dir-'));
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns a flat array (not wrapped in { entries })', () => {
|
||||||
|
fs.writeFileSync(path.join(tmpDir, 'a.md'), 'hello');
|
||||||
|
const result = listDirectoryEntries(tmpDir);
|
||||||
|
expect(Array.isArray(result)).toBe(true);
|
||||||
|
expect(result.length).toBe(1);
|
||||||
|
expect(result[0]).toMatchObject({
|
||||||
|
name: 'a.md',
|
||||||
|
isDirectory: false,
|
||||||
|
size: 5,
|
||||||
|
path: path.join(tmpDir, 'a.md'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('excludes dotfiles', () => {
|
||||||
|
fs.writeFileSync(path.join(tmpDir, '.hidden'), 'x');
|
||||||
|
fs.writeFileSync(path.join(tmpDir, 'visible.md'), 'y');
|
||||||
|
const result = listDirectoryEntries(tmpDir);
|
||||||
|
expect(result.map((e) => e.name)).toEqual(['visible.md']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sorts directories first then alphabetically', () => {
|
||||||
|
fs.writeFileSync(path.join(tmpDir, 'zebra.md'), 'z');
|
||||||
|
fs.mkdirSync(path.join(tmpDir, 'aaa'));
|
||||||
|
fs.mkdirSync(path.join(tmpDir, 'bbb'));
|
||||||
|
fs.writeFileSync(path.join(tmpDir, 'apple.md'), 'a');
|
||||||
|
const result = listDirectoryEntries(tmpDir);
|
||||||
|
expect(result.map((e) => e.name)).toEqual(['aaa', 'bbb', 'apple.md', 'zebra.md']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('marks directories with isDirectory: true and size: 0', () => {
|
||||||
|
fs.mkdirSync(path.join(tmpDir, 'sub'));
|
||||||
|
const result = listDirectoryEntries(tmpDir);
|
||||||
|
expect(result[0]).toMatchObject({ name: 'sub', isDirectory: true, size: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skips entries that cannot be stat\'d (broken symlinks)', () => {
|
||||||
|
fs.writeFileSync(path.join(tmpDir, 'good.md'), 'ok');
|
||||||
|
try {
|
||||||
|
fs.symlinkSync(path.join(tmpDir, 'nonexistent'), path.join(tmpDir, 'broken-link'));
|
||||||
|
} catch (_) {
|
||||||
|
// symlinks not supported in env — skip
|
||||||
|
}
|
||||||
|
const result = listDirectoryEntries(tmpDir);
|
||||||
|
expect(result.find((e) => e.name === 'good.md')).toBeTruthy();
|
||||||
|
expect(result.find((e) => e.name === 'broken-link')).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user