mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 17:29:29 +05:30
Daily notes panel:
- src/sidebar/daily-notes-panel.js — new sidebar panel that lists every
YYYY-MM-DD.md in the daily-notes dir (newest first), with a Today
button that creates/opens today's entry. Refresh button reloads.
Clicking a row calls onOpenFile(path). DOM is built with textContent
for dynamic fields — no XSS surface from a hostile filename.
- src/main.js — daily-notes:list IPC now returns absolute paths (joined
with the daily-notes dir) so the renderer can pass them straight to
open-file-path without re-synthesizing.
- src/renderer.js — registers the panel; icon for the daily-notes
button (calendar icon).
- src/index.html — calendar SVG icon for the daily-notes sidebar entry.
Q&A deep-link:
- src/main.js — open-file-path accepts either a string (legacy) or an
object {path, offset}. The offset is forwarded to the renderer via
file-opened.
- src/renderer.js — file-opened handler scrolls the editor to the
offset using EditorView.scrollIntoView(y: 'center') so the matching
passage lands in the middle of the visible area.
- search panel now passes {path, offset} to open-file-path so Q&A
results with chunk offsets jump straight to the passage.
Tests (12 new, tests/daily-notes-panel.test.js):
- mount, empty state, list rendering
- Today button → onOpenFile, 'Created today' / 'Today already exists' status
- error paths (openToday reject, listExisting reject, listExisting missing)
- click + Enter/Space on a row open the right path
- refresh() re-fetches, keeps status when keepStatus:true
- non-md entries filtered out
- XSS-safe textContent for dynamic data
Full suite: 73 suites, 873 tests, lint+format clean.
Amit Haridas
175 lines
6.5 KiB
JavaScript
175 lines
6.5 KiB
JavaScript
/**
|
|
* @jest-environment jsdom
|
|
*
|
|
* Daily notes sidebar panel tests.
|
|
*/
|
|
const { renderDailyNotesPanel } = require('../src/sidebar/daily-notes-panel');
|
|
|
|
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 }),
|
|
listExisting: deps.listExisting || jest.fn().mockResolvedValue([]),
|
|
onOpenFile: deps.onOpenFile || jest.fn(),
|
|
...deps,
|
|
});
|
|
return { container, api };
|
|
}
|
|
|
|
async function flush() {
|
|
for (let i = 0; i < 5; i++) await Promise.resolve();
|
|
}
|
|
|
|
describe('renderDailyNotesPanel — mounting', () => {
|
|
test('mounts the panel structure', () => {
|
|
const { container } = mountPanel();
|
|
expect(container.querySelector('.daily-notes-panel')).not.toBeNull();
|
|
expect(container.querySelector('#daily-notes-today')).not.toBeNull();
|
|
expect(container.querySelector('#daily-notes-refresh')).not.toBeNull();
|
|
expect(container.querySelector('#daily-notes-list')).not.toBeNull();
|
|
});
|
|
|
|
test('shows the empty-state message when there are no notes', async () => {
|
|
const { container } = mountPanel({ listExisting: jest.fn().mockResolvedValue([]) });
|
|
await flush();
|
|
expect(container.querySelector('.daily-notes-empty')).not.toBeNull();
|
|
expect(container.textContent).toMatch(/Press.*Today/);
|
|
});
|
|
|
|
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',
|
|
]),
|
|
});
|
|
await flush();
|
|
const items = container.querySelectorAll('.daily-notes-item');
|
|
expect(items).toHaveLength(3);
|
|
expect(items[0].textContent).toContain('2026-09-14');
|
|
expect(items[0].textContent).toMatch(/Sep/);
|
|
});
|
|
});
|
|
|
|
describe('renderDailyNotesPanel — Today button', () => {
|
|
test('clicking Today opens (or creates) today and calls onOpenFile with the path', async () => {
|
|
const openToday = jest.fn().mockResolvedValue({
|
|
path: '/notes/2026-09-14.md',
|
|
created: true,
|
|
content: '',
|
|
});
|
|
const onOpenFile = jest.fn();
|
|
const listExisting = jest.fn().mockResolvedValue(['2026-09-14.md']);
|
|
const { container } = mountPanel({ openToday, onOpenFile, listExisting });
|
|
await flush();
|
|
|
|
container.querySelector('#daily-notes-today').click();
|
|
await flush();
|
|
|
|
expect(openToday).toHaveBeenCalledWith({});
|
|
expect(onOpenFile).toHaveBeenCalledWith('/notes/2026-09-14.md');
|
|
});
|
|
|
|
test('shows a "Created today." / "Today already exists." status depending on the result', async () => {
|
|
const onOpenFile = jest.fn();
|
|
const { container } = mountPanel({
|
|
openToday: jest.fn().mockResolvedValue({ path: '/x.md', created: true }),
|
|
listExisting: jest.fn().mockResolvedValue([]),
|
|
onOpenFile,
|
|
});
|
|
await flush();
|
|
container.querySelector('#daily-notes-today').click();
|
|
await flush();
|
|
expect(container.querySelector('#daily-notes-status').textContent).toMatch(/Created today/);
|
|
|
|
// Re-mount with a "not created" result
|
|
const { container: c2 } = mountPanel({
|
|
openToday: jest.fn().mockResolvedValue({ path: '/x.md', created: false }),
|
|
listExisting: jest.fn().mockResolvedValue([]),
|
|
onOpenFile,
|
|
});
|
|
await flush();
|
|
c2.querySelector('#daily-notes-today').click();
|
|
await flush();
|
|
expect(c2.querySelector('#daily-notes-status').textContent).toMatch(/already exists/);
|
|
});
|
|
|
|
test('handles openToday errors gracefully (no crash, status reports)', async () => {
|
|
const openToday = jest.fn().mockRejectedValue(new Error('disk gone'));
|
|
const { container } = mountPanel({ openToday });
|
|
await flush();
|
|
container.querySelector('#daily-notes-today').click();
|
|
await flush();
|
|
expect(container.querySelector('#daily-notes-status').textContent).toMatch(/disk gone/);
|
|
});
|
|
});
|
|
|
|
describe('renderDailyNotesPanel — click an existing entry', () => {
|
|
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',
|
|
]),
|
|
});
|
|
await flush();
|
|
const items = container.querySelectorAll('.daily-notes-item');
|
|
items[1].click();
|
|
expect(onOpenFile).toHaveBeenCalledWith('/notes/daily/2026-09-10.md');
|
|
});
|
|
|
|
test('Enter / Space on a focused row opens it', async () => {
|
|
const onOpenFile = jest.fn();
|
|
const { container } = mountPanel({
|
|
onOpenFile,
|
|
listExisting: jest.fn().mockResolvedValue(['/notes/daily/2026-09-14.md']),
|
|
});
|
|
await flush();
|
|
const row = container.querySelector('.daily-notes-item');
|
|
row.focus();
|
|
row.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
await flush();
|
|
expect(onOpenFile).toHaveBeenCalledWith('/notes/daily/2026-09-14.md');
|
|
});
|
|
});
|
|
|
|
describe('renderDailyNotesPanel — refresh', () => {
|
|
test('refresh button re-fetches the list', async () => {
|
|
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);
|
|
|
|
api.refresh();
|
|
await flush();
|
|
expect(container.querySelectorAll('.daily-notes-item')).toHaveLength(1);
|
|
expect(listExisting).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
describe('renderDailyNotesPanel — graceful degradation', () => {
|
|
test('shows an error when listExisting rejects', async () => {
|
|
const listExisting = jest.fn().mockRejectedValue(new Error('ENOENT'));
|
|
const { container } = mountPanel({ listExisting });
|
|
await flush();
|
|
expect(container.querySelector('#daily-notes-status').textContent).toMatch(/ENOENT/);
|
|
});
|
|
|
|
test('shows an error when listExisting is not provided', async () => {
|
|
const { container } = mountPanel({ listExisting: null });
|
|
await flush();
|
|
expect(container.querySelector('#daily-notes-status').textContent).toMatch(/unavailable/);
|
|
});
|
|
|
|
test('filters out non-md entries', async () => {
|
|
const { container } = mountPanel({
|
|
listExisting: jest.fn().mockResolvedValue(['2026-09-14.md', 'readme.txt', '.DS_Store']),
|
|
});
|
|
await flush();
|
|
expect(container.querySelectorAll('.daily-notes-item')).toHaveLength(1);
|
|
});
|
|
}); |