mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 17:29:29 +05:30
chore(release): bump v4.7.1 → v4.8.0; Prettier pass on overnight-session files
12 commits on master since v4.7.1 baseline, taking tests 524 → 914 across 27 new suites. This commit: - Bumps package.json + README version to v4.8.0 (additive features → semver minor) - Applies Prettier formatting to all files touched during the overnight session so release build is reproducible from a clean repo New in v4.8.0: - PDF encryption close-out (D1) - KaTeX rendering test coverage - Autosave buffer + crash recovery banner - Daily notes + workspace search + doc-aware Q&A - Search sidebar panel + Ask mode - Status bar: word count, reading time, Flesch-Kincaid grade - Footnote hover preview - Smart paste (URL → link, CSV/TSV → table) - Daily notes menu item + sidebar panel + Q&A deep-link - Daily templates gallery - Pluggable DocQA engine (TF-idF default + lazy neural embeddings) Amit Haridas
This commit is contained in:
@@ -3,7 +3,13 @@
|
||||
*
|
||||
* csv-to-table tests — pure module.
|
||||
*/
|
||||
const { detectDelimiter, csvToTable, looksLikeCsv, parseRows, escapeCell } = require('../src/utils/csv-to-table');
|
||||
const {
|
||||
detectDelimiter,
|
||||
csvToTable,
|
||||
looksLikeCsv,
|
||||
parseRows,
|
||||
escapeCell,
|
||||
} = require('../src/utils/csv-to-table');
|
||||
|
||||
describe('detectDelimiter', () => {
|
||||
test('prefers tabs over commas when both are present', () => {
|
||||
@@ -155,7 +161,7 @@ describe('looksLikeCsv', () => {
|
||||
expect(looksLikeCsv('a,b,c')).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false when the column counts don\'t line up at all', () => {
|
||||
test("returns false when the column counts don't line up at all", () => {
|
||||
expect(looksLikeCsv('a,b,c\n1\n2,3,4,5')).toBe(false);
|
||||
});
|
||||
|
||||
@@ -166,6 +172,8 @@ describe('looksLikeCsv', () => {
|
||||
});
|
||||
|
||||
test('returns false for prose (no delimiter)', () => {
|
||||
expect(looksLikeCsv('The quick brown fox jumps over the lazy dog.\nThe cat sat on the mat.')).toBe(false);
|
||||
expect(
|
||||
looksLikeCsv('The quick brown fox jumps over the lazy dog.\nThe cat sat on the mat.')
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,9 @@ function mountPanel(deps = {}) {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const api = renderDailyNotesPanel(container, {
|
||||
openToday: deps.openToday || jest.fn().mockResolvedValue({ path: '/notes/2026-09-14.md', created: true }),
|
||||
openToday:
|
||||
deps.openToday ||
|
||||
jest.fn().mockResolvedValue({ path: '/notes/2026-09-14.md', created: true }),
|
||||
listExisting: deps.listExisting || jest.fn().mockResolvedValue([]),
|
||||
onOpenFile: deps.onOpenFile || jest.fn(),
|
||||
...deps,
|
||||
@@ -39,11 +41,13 @@ describe('renderDailyNotesPanel — mounting', () => {
|
||||
|
||||
test('renders one row per filename', async () => {
|
||||
const { container } = mountPanel({
|
||||
listExisting: jest.fn().mockResolvedValue([
|
||||
'/notes/daily/2026-09-14.md',
|
||||
'/notes/daily/2026-09-13.md',
|
||||
'/notes/daily/2026-09-10.md',
|
||||
]),
|
||||
listExisting: jest
|
||||
.fn()
|
||||
.mockResolvedValue([
|
||||
'/notes/daily/2026-09-14.md',
|
||||
'/notes/daily/2026-09-13.md',
|
||||
'/notes/daily/2026-09-10.md',
|
||||
]),
|
||||
});
|
||||
await flush();
|
||||
const items = container.querySelectorAll('.daily-notes-item');
|
||||
@@ -107,14 +111,13 @@ describe('renderDailyNotesPanel — Today button', () => {
|
||||
});
|
||||
|
||||
describe('renderDailyNotesPanel — click an existing entry', () => {
|
||||
test('clicking a row calls onOpenFile with that row\'s path', async () => {
|
||||
test("clicking a row calls onOpenFile with that row's path", async () => {
|
||||
const onOpenFile = jest.fn();
|
||||
const { container } = mountPanel({
|
||||
onOpenFile,
|
||||
listExisting: jest.fn().mockResolvedValue([
|
||||
'/notes/daily/2026-09-14.md',
|
||||
'/notes/daily/2026-09-10.md',
|
||||
]),
|
||||
listExisting: jest
|
||||
.fn()
|
||||
.mockResolvedValue(['/notes/daily/2026-09-14.md', '/notes/daily/2026-09-10.md']),
|
||||
});
|
||||
await flush();
|
||||
const items = container.querySelectorAll('.daily-notes-item');
|
||||
@@ -139,7 +142,10 @@ describe('renderDailyNotesPanel — click an existing entry', () => {
|
||||
|
||||
describe('renderDailyNotesPanel — refresh', () => {
|
||||
test('refresh button re-fetches the list', async () => {
|
||||
const listExisting = jest.fn().mockResolvedValueOnce([]).mockResolvedValueOnce(['2026-09-14.md']);
|
||||
const listExisting = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce(['2026-09-14.md']);
|
||||
const { container, api } = mountPanel({ listExisting });
|
||||
await flush();
|
||||
expect(container.querySelectorAll('.daily-notes-item')).toHaveLength(0);
|
||||
@@ -172,4 +178,4 @@ describe('renderDailyNotesPanel — graceful degradation', () => {
|
||||
await flush();
|
||||
expect(container.querySelectorAll('.daily-notes-item')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,9 +10,12 @@ function mountPanel(deps = {}) {
|
||||
document.body.appendChild(container);
|
||||
const api = renderDailyTemplatesPanel(container, {
|
||||
listTemplates: deps.listTemplates || jest.fn().mockResolvedValue([]),
|
||||
saveTemplate: deps.saveTemplate || jest.fn().mockResolvedValue({ name: 'x.md', label: 'X', content: '' }),
|
||||
saveTemplate:
|
||||
deps.saveTemplate || jest.fn().mockResolvedValue({ name: 'x.md', label: 'X', content: '' }),
|
||||
deleteTemplate: deps.deleteTemplate || jest.fn().mockResolvedValue(true),
|
||||
applyTemplate: deps.applyTemplate || jest.fn().mockResolvedValue({ path: '/d/2026-09-14.md', content: '', created: true }),
|
||||
applyTemplate:
|
||||
deps.applyTemplate ||
|
||||
jest.fn().mockResolvedValue({ path: '/d/2026-09-14.md', content: '', created: true }),
|
||||
onOpenFile: deps.onOpenFile || jest.fn(),
|
||||
...deps,
|
||||
});
|
||||
@@ -53,9 +56,9 @@ describe('renderDailyTemplatesPanel — mounting', () => {
|
||||
|
||||
test('escapeHtml-style safety: a label with <script> does not produce a node', async () => {
|
||||
const { container } = mountPanel({
|
||||
listTemplates: jest.fn().mockResolvedValue([
|
||||
{ name: 'evil.md', label: '<script>alert(1)</script>', content: '' },
|
||||
]),
|
||||
listTemplates: jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ name: 'evil.md', label: '<script>alert(1)</script>', content: '' }]),
|
||||
});
|
||||
await flush();
|
||||
expect(container.querySelector('script')).toBeNull();
|
||||
@@ -72,7 +75,9 @@ describe('renderDailyTemplatesPanel — Use button', () => {
|
||||
const { container } = mountPanel({
|
||||
applyTemplate,
|
||||
onOpenFile,
|
||||
listTemplates: jest.fn().mockResolvedValue([{ name: 'morning.md', label: 'Morning', content: '# M' }]),
|
||||
listTemplates: jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ name: 'morning.md', label: 'Morning', content: '# M' }]),
|
||||
});
|
||||
await flush();
|
||||
container.querySelector('[data-action="apply"]').click();
|
||||
@@ -83,7 +88,9 @@ describe('renderDailyTemplatesPanel — Use button', () => {
|
||||
|
||||
test('reports an error when applyTemplate rejects', async () => {
|
||||
const applyTemplate = jest.fn().mockRejectedValue(new Error('write fail'));
|
||||
const listTemplates = jest.fn().mockResolvedValue([{ name: 'morning.md', label: 'Morning', content: '' }]);
|
||||
const listTemplates = jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ name: 'morning.md', label: 'Morning', content: '' }]);
|
||||
const { container } = mountPanel({ applyTemplate, listTemplates });
|
||||
await flush();
|
||||
container.querySelector('[data-action="apply"]').click();
|
||||
@@ -128,11 +135,10 @@ describe('renderDailyTemplatesPanel — Delete button', () => {
|
||||
|
||||
describe('renderDailyTemplatesPanel — New template', () => {
|
||||
test('clicking + New prompts for name + content, then saves', async () => {
|
||||
const saveTemplate = jest.fn().mockResolvedValue({ name: 'custom.md', label: 'Custom', content: '# X' });
|
||||
window.prompt = jest
|
||||
const saveTemplate = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce('custom')
|
||||
.mockReturnValueOnce('# X');
|
||||
.mockResolvedValue({ name: 'custom.md', label: 'Custom', content: '# X' });
|
||||
window.prompt = jest.fn().mockReturnValueOnce('custom').mockReturnValueOnce('# X');
|
||||
|
||||
const { container } = mountPanel({ saveTemplate });
|
||||
await flush();
|
||||
@@ -158,9 +164,10 @@ describe('renderDailyTemplatesPanel — New template', () => {
|
||||
|
||||
describe('renderDailyTemplatesPanel — refresh', () => {
|
||||
test('refresh button re-fetches the list', async () => {
|
||||
const listTemplates = jest.fn().mockResolvedValueOnce([]).mockResolvedValueOnce([
|
||||
{ name: 'a.md', label: 'A', content: '' },
|
||||
]);
|
||||
const listTemplates = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([{ name: 'a.md', label: 'A', content: '' }]);
|
||||
const { container, api } = mountPanel({ listTemplates });
|
||||
await flush();
|
||||
expect(container.querySelectorAll('.daily-templates-item')).toHaveLength(0);
|
||||
@@ -172,8 +179,10 @@ describe('renderDailyTemplatesPanel — refresh', () => {
|
||||
});
|
||||
|
||||
test('shows an error when listTemplates rejects', async () => {
|
||||
const { container } = mountPanel({ listTemplates: jest.fn().mockRejectedValue(new Error('nope')) });
|
||||
const { container } = mountPanel({
|
||||
listTemplates: jest.fn().mockRejectedValue(new Error('nope')),
|
||||
});
|
||||
await flush();
|
||||
expect(container.querySelector('#daily-templates-status').textContent).toMatch(/nope/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,7 +151,9 @@ describe('mountFootnotePreview', () => {
|
||||
document.body.appendChild(root);
|
||||
|
||||
const unmount = mountFootnotePreview(root, { delayMs: 0 });
|
||||
root.querySelector('a[data-footnote-ref]').dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
||||
root
|
||||
.querySelector('a[data-footnote-ref]')
|
||||
.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
||||
jest.advanceTimersByTime(0);
|
||||
expect(document.querySelector('.footnote-preview')).not.toBeNull();
|
||||
|
||||
@@ -160,8 +162,10 @@ describe('mountFootnotePreview', () => {
|
||||
expect(document.querySelector('.footnote-preview')).toBeNull();
|
||||
|
||||
// Re-hovering after unmount is a no-op (no popover recreated)
|
||||
root.querySelector('a[data-footnote-ref]').dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
||||
root
|
||||
.querySelector('a[data-footnote-ref]')
|
||||
.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
||||
jest.advanceTimersByTime(0);
|
||||
expect(document.querySelector('.footnote-preview')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,4 +168,4 @@ describe('DailyNotesTemplates.deleteTemplate', () => {
|
||||
expect(DailyNotesTemplates.deleteTemplate({ name: 'x', fs, pathUtil: path })).toBe(false);
|
||||
expect(DailyNotesTemplates.deleteTemplate({ dir: tmpDir, fs, pathUtil: path })).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,9 +144,7 @@ describe('DocQA.ask with a custom engine', () => {
|
||||
test('passes the question + chunks to a custom engine.rank()', async () => {
|
||||
const customEngine = {
|
||||
isNeural: true,
|
||||
rank: jest.fn().mockResolvedValue([
|
||||
{ filePath: '/x.md#0', snippet: 'ranked', score: 0.9 },
|
||||
]),
|
||||
rank: jest.fn().mockResolvedValue([{ filePath: '/x.md#0', snippet: 'ranked', score: 0.9 }]),
|
||||
};
|
||||
const r = await DocQA.ask({ question: 'rust', files, engine: customEngine });
|
||||
expect(customEngine.rank).toHaveBeenCalled();
|
||||
|
||||
@@ -54,8 +54,7 @@ describe('PDFOperations - Task 15 new operations', () => {
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.text).toContain('Hello Task 15 Page One');
|
||||
expect(result.text).toContain('Second Page Content');
|
||||
}, // CI runners (observed on windows-latest) // First pdfjs-dist legacy import can exceed the 5s default on slower
|
||||
30000);
|
||||
}, 30000); // CI runners (observed on windows-latest) // First pdfjs-dist legacy import can exceed the 5s default on slower
|
||||
|
||||
it('returns failure for a nonexistent file', async () => {
|
||||
const result = await PDFOperations.pdfExtractText({
|
||||
|
||||
@@ -85,4 +85,4 @@ describe('SemanticEngine — WorkspaceSearch parity', () => {
|
||||
expect(engineResult.map((r) => r.filePath)).toEqual(directResult.map((r) => r.filePath));
|
||||
expect(engineResult[0].score).toBe(directResult[0].score);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,7 +57,9 @@ describe('UrlTitle.decodeTitle', () => {
|
||||
describe('UrlTitle.extractTitleFromHtml', () => {
|
||||
test('extracts the first <title> in the body', () => {
|
||||
expect(
|
||||
UrlTitle.extractTitleFromHtml('<html><head><title>Hello World</title></head><body></body></html>')
|
||||
UrlTitle.extractTitleFromHtml(
|
||||
'<html><head><title>Hello World</title></head><body></body></html>'
|
||||
)
|
||||
).toBe('Hello World');
|
||||
});
|
||||
|
||||
@@ -174,7 +176,9 @@ describe('UrlTitle.fetchTitle — failure paths', () => {
|
||||
headers: { get: () => 'text/html' },
|
||||
text: async () => '<title>t</title>' + 'x'.repeat(200),
|
||||
});
|
||||
expect(await UrlTitle.fetchTitle({ url: 'https://x.com', fetch: fakeFetch, maxBytes: 100 })).toBeNull();
|
||||
expect(
|
||||
await UrlTitle.fetchTitle({ url: 'https://x.com', fetch: fakeFetch, maxBytes: 100 })
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('respects a streaming body that exceeds maxBytes (cancels the reader)', async () => {
|
||||
@@ -195,4 +199,4 @@ describe('UrlTitle.fetchTitle — failure paths', () => {
|
||||
expect(r).toBeNull();
|
||||
expect(cancel).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,9 +108,9 @@ describe('renderSearchPanel — search mode', () => {
|
||||
|
||||
test('clicking a result calls onOpenFile with the right path', async () => {
|
||||
const onOpenFile = jest.fn();
|
||||
const search = jest.fn().mockResolvedValue([
|
||||
{ filePath: '/notes/x.md', snippet: 's', score: 1, matchedTerms: [] },
|
||||
]);
|
||||
const search = jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ filePath: '/notes/x.md', snippet: 's', score: 1, matchedTerms: [] }]);
|
||||
const { container } = mountPanel({ search, onOpenFile });
|
||||
container.querySelector('#search-input').value = 'x';
|
||||
container.querySelector('#search-run').click();
|
||||
@@ -246,9 +246,7 @@ describe('renderSearchPanel — escaping', () => {
|
||||
|
||||
describe('renderSearchPanel — clear/escape behaviors', () => {
|
||||
test('Escape on the input clears the query and results', async () => {
|
||||
const search = jest.fn().mockResolvedValue([
|
||||
{ filePath: '/a.md', snippet: 's', score: 1 },
|
||||
]);
|
||||
const search = jest.fn().mockResolvedValue([{ filePath: '/a.md', snippet: 's', score: 1 }]);
|
||||
const { container, container: c2 } = mountPanel({ search });
|
||||
const input = container.querySelector('#search-input');
|
||||
input.value = 'x';
|
||||
@@ -283,4 +281,4 @@ describe('renderSearchPanel — clear/escape behaviors', () => {
|
||||
api.clear();
|
||||
expect(container.querySelector('#search-input').value).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,4 +71,4 @@ describe('Smart paste replacement formatting', () => {
|
||||
const replacement = `[T](${url})`;
|
||||
expect(replacement).toBe('[T](https://example.com/path?a=1&b=2#frag)');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,9 @@ describe('stripMarkdown', () => {
|
||||
|
||||
test('keeps link text but drops URLs', () => {
|
||||
expect(stripMarkdown('read [the docs](https://docs.example.com) now')).toMatch(/the docs/);
|
||||
expect(stripMarkdown('read [the docs](https://docs.example.com) now')).not.toMatch(/docs\.example/);
|
||||
expect(stripMarkdown('read [the docs](https://docs.example.com) now')).not.toMatch(
|
||||
/docs\.example/
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps wikilink visible labels', () => {
|
||||
@@ -177,4 +179,4 @@ A second paragraph with [a link](https://x.com) and **bold** words.`;
|
||||
const s = computeStats(' hello world ');
|
||||
expect(s.charCount).toBe(10); // "helloworld"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user