feat: bookmarklet to blog stuff easily

This commit is contained in:
2026-02-22 17:49:11 +01:00
parent 6e45936fc9
commit 509afa4c85
17 changed files with 613 additions and 5 deletions

View File

@@ -0,0 +1,41 @@
import { describe, it, expect } from 'vitest';
import {
buildBlogmarkMarkdownLink,
extractBlogmarkPayloadFromDeepLink,
generateBlogmarkBookmarkletSource,
} from '../../src/main/shared/blogmark';
describe('blogmark deep-link payload', () => {
it('extracts and sanitizes title and URL from deep link', () => {
const payload = extractBlogmarkPayloadFromDeepLink(
'bds://new-post?title=Hello%20%3Cb%3EWorld%3C%2Fb%3E&url=https%3A%2F%2Fexample.com%2Fpost%3Fx%3D1%23frag',
);
expect(payload).toEqual({
title: 'Hello <b>World</b>',
url: 'https://example.com/post?x=1',
});
});
it('rejects non-http URLs', () => {
const payload = extractBlogmarkPayloadFromDeepLink(
'bds://new-post?title=Unsafe&url=javascript%3Aalert(1)',
);
expect(payload).toBeNull();
});
it('builds safe markdown source link', () => {
const markdown = buildBlogmarkMarkdownLink('A [title] (test)', 'https://example.com/x?y=1');
expect(markdown).toBe('[A \\[title\\] \\(test\\)](<https://example.com/x?y=1>)');
});
it('generates bookmarklet that targets bds protocol', () => {
const source = generateBlogmarkBookmarkletSource();
expect(source.startsWith('javascript:')).toBe(true);
expect(source).toContain('bds://new-post?title=');
expect(source).toContain('encodeURIComponent(document.title');
expect(source).toContain('encodeURIComponent(location.href');
});
});

View File

@@ -729,6 +729,30 @@ describe('MetaEngine', () => {
expect(metadata?.maxPostsPerPage).toBe(42);
});
it('should set and get blogmarkCategory in project metadata', async () => {
await metaEngine.setProjectMetadata({
name: 'My Blog',
blogmarkCategory: 'Article',
} as any);
const metadata = await metaEngine.getProjectMetadata();
expect((metadata as any)?.blogmarkCategory).toBe('article');
});
it('should persist blogmarkCategory to filesystem', async () => {
await metaEngine.setProjectMetadata({
name: 'Test Project',
blogmarkCategory: 'links',
} as any);
const metaDir = metaEngine.getMetaDir();
const projectPath = normalizePath(`${metaDir}/project.json`);
const content = mockFiles.get(projectPath);
const parsed = JSON.parse(content!);
expect(parsed.blogmarkCategory).toBe('links');
});
it('should set and get publicUrl in project metadata', async () => {
await metaEngine.setProjectMetadata({
name: 'My Blog',

View File

@@ -647,4 +647,156 @@ describe('main bootstrap preview behavior', () => {
height: 775,
}));
});
it('handles bds deep-link by creating a blogmark post with preferred category', async () => {
const listeners = new Map<string, (...args: any[]) => void>();
const mockApp = {
name: 'bDS',
whenReady: vi.fn(() => Promise.resolve()),
on: vi.fn((event: string, callback: (...args: any[]) => void) => {
listeners.set(event, callback);
}),
quit: vi.fn(),
requestSingleInstanceLock: vi.fn(() => true),
setAsDefaultProtocolClient: vi.fn(() => true),
};
const windows: Array<{ webContents: { send: ReturnType<typeof vi.fn> } }> = [];
class MockBrowserWindow {
static getAllWindows = vi.fn(() => windows as any);
loadURL = vi.fn();
loadFile = vi.fn();
on = vi.fn();
isDestroyed = vi.fn(() => false);
webContents = {
on: vi.fn(),
send: vi.fn(),
openDevTools: vi.fn(),
toggleDevTools: vi.fn(),
};
constructor() {
windows.push(this as any);
}
}
vi.doMock('electron', () => ({
app: mockApp,
BrowserWindow: MockBrowserWindow,
Menu: {
buildFromTemplate: vi.fn(() => ({})),
setApplicationMenu: vi.fn(),
},
ipcMain: {
on: vi.fn(),
handle: vi.fn(),
removeHandler: vi.fn(),
},
protocol: {
registerSchemesAsPrivileged: vi.fn(),
handle: vi.fn(),
},
net: {
fetch: vi.fn(),
},
shell: {
openExternal: vi.fn(),
openPath: vi.fn(),
},
}));
class MockPreviewServer {
start = vi.fn().mockResolvedValue(4123);
stop = vi.fn().mockResolvedValue(undefined);
getBaseUrl = vi.fn(() => 'http://127.0.0.1:4123');
}
const createPost = vi.fn().mockResolvedValue({
id: 'new-post-id',
title: 'Example title',
content: '[Example title](<https://example.com/>)',
categories: ['article'],
});
vi.doMock('../../src/main/engine/PreviewServer', () => ({
PreviewServer: MockPreviewServer,
}));
vi.doMock('../../src/main/engine/PostEngine', () => ({
getPostEngine: vi.fn(() => ({
getPost: vi.fn().mockResolvedValue(null),
createPost,
})),
}));
vi.doMock('../../src/main/engine/MetaEngine', () => ({
getMetaEngine: vi.fn(() => ({
getProjectMetadata: vi.fn().mockResolvedValue({ blogmarkCategory: 'article' }),
})),
}));
vi.doMock('../../src/main/database', () => ({
getDatabase: vi.fn(() => ({
initializeLocal: vi.fn().mockResolvedValue(undefined),
close: vi.fn().mockResolvedValue(undefined),
getLocal: vi.fn(() => ({
select: vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({
get: vi.fn().mockResolvedValue(null),
})),
})),
})),
})),
getDataPaths: vi.fn(() => ({ database: '/tmp/mock.db' })),
})),
}));
vi.doMock('../../src/main/ipc', () => ({
registerIpcHandlers: vi.fn(),
registerChatHandlers: vi.fn(),
initializeChatHandlers: vi.fn(),
cleanupChatHandlers: vi.fn().mockResolvedValue(undefined),
}));
vi.doMock('../../src/main/database/schema', () => ({
media: {},
}));
vi.doMock('drizzle-orm', () => ({
eq: vi.fn(),
}));
vi.doMock('../../src/main/engine/MediaEngine', () => ({
getMediaEngine: vi.fn(() => ({
getThumbnailPaths: vi.fn().mockResolvedValue({ small: null }),
})),
}));
await import('../../src/main/main');
await new Promise((resolve) => setTimeout(resolve, 0));
const openUrl = listeners.get('open-url');
expect(openUrl).toBeTruthy();
const preventDefault = vi.fn();
openUrl?.({ preventDefault } as any, 'bds://new-post?title=Example%20title&url=https%3A%2F%2Fexample.com%2F');
await new Promise((resolve) => setTimeout(resolve, 0));
expect(preventDefault).toHaveBeenCalled();
expect(createPost).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Example title',
content: '[Example title](<https://example.com/>)',
categories: ['article'],
}),
);
expect(windows[0]?.webContents.send).toHaveBeenCalledWith(
'blogmark:created',
expect.objectContaining({ id: 'new-post-id' }),
);
});
});