feat: preview server startup directly

This commit is contained in:
2026-02-16 21:28:16 +01:00
parent 5d0791566e
commit 54a8ba5ceb
4 changed files with 216 additions and 2 deletions

View File

@@ -8,6 +8,8 @@ import { getProjectEngine } from './ProjectEngine';
interface ActiveProjectContext {
projectId: string;
dataDir?: string;
projectName?: string;
projectDescription?: string;
}
interface PostEngineContract {
@@ -57,6 +59,30 @@ function clampMaxPostsPerPage(value: unknown): number {
return normalized;
}
function resolvePageTitle(metadata: ProjectMetadata | null, fallbackProjectName?: string, fallbackProjectDescription?: string): string {
const candidate = metadata?.description?.trim();
if (candidate) {
return candidate;
}
const metadataName = metadata?.name?.trim();
if (metadataName) {
return metadataName;
}
const descriptionFallback = fallbackProjectDescription?.trim();
if (descriptionFallback) {
return descriptionFallback;
}
const fallback = fallbackProjectName?.trim();
if (fallback) {
return fallback;
}
return 'Blog Preview';
}
function escapeHtml(value: string): string {
return value
.replace(/&/g, '&')
@@ -162,7 +188,12 @@ export class PreviewServer {
const activeProject = await projectEngine.getActiveProject();
const projectId = activeProject?.id ?? 'default';
const dataDir = projectEngine.getDataDir(projectId, activeProject?.dataPath);
return { projectId, dataDir };
return {
projectId,
dataDir,
projectName: activeProject?.name,
projectDescription: activeProject?.description ?? undefined,
};
});
}
@@ -267,7 +298,7 @@ export class PreviewServer {
return;
}
this.respond(res, 200, getPageHtml(result, 'Blog Preview'));
this.respond(res, 200, getPageHtml(result, resolvePageTitle(metadata, context.projectName, context.projectDescription)));
} catch (error) {
console.error('[PreviewServer] Request failed:', error);
this.respond(res, 500, 'Internal Server Error');

View File

@@ -109,6 +109,14 @@ async function openPreviewInBrowser(): Promise<void> {
await shell.openExternal(`${previewServer.getBaseUrl()}/`);
}
async function startPreviewServerOnAppStart(): Promise<void> {
if (!previewServer) {
previewServer = new PreviewServer();
}
await previewServer.start(PREVIEW_SERVER_PORT);
}
function createApplicationMenu(): Menu {
const template: MenuItemConstructorOptions[] = [
{
@@ -445,6 +453,11 @@ async function initialize(): Promise<void> {
// App lifecycle
app.whenReady().then(async () => {
await initialize();
try {
await startPreviewServerOnAppStart();
} catch (error) {
console.error('Failed to start preview server on app startup:', error);
}
createWindow();
app.on('activate', () => {

View File

@@ -275,4 +275,57 @@ describe('PreviewServer', () => {
const renderedPosts = (html.match(/<div class="post">/g) || []).length;
expect(renderedPosts).toBe(7);
});
it('uses project description from metadata in page title', async () => {
server = new PreviewServer({
postEngine: makeEngine([makePost()]),
settingsEngine: {
setProjectContext: vi.fn(),
async getProjectMetadata() {
return {
name: 'My Great Blog',
description: 'A wonderful publication',
maxPostsPerPage: 50,
};
},
},
getActiveProjectContext: async () => ({ projectId: 'default' }),
});
await server.start(0);
const response = await fetch(`${server.getBaseUrl()}/`);
expect(response.status).toBe(200);
const html = await response.text();
expect(html).toContain('<title>A wonderful publication</title>');
expect(html).not.toContain('<title>My Great Blog</title>');
expect(html).not.toContain('<title>Blog Preview</title>');
});
it('falls back to active project name in page title when metadata is unavailable', async () => {
server = new PreviewServer({
postEngine: makeEngine([makePost()]),
settingsEngine: {
setProjectContext: vi.fn(),
async getProjectMetadata() {
return null;
},
},
getActiveProjectContext: async () => ({
projectId: 'default',
dataDir: '/tmp/default',
projectName: 'Configured Project Name',
} as any),
});
await server.start(0);
const response = await fetch(`${server.getBaseUrl()}/`);
expect(response.status).toBe(200);
const html = await response.text();
expect(html).toContain('<title>Configured Project Name</title>');
expect(html).not.toContain('<title>Blog Preview</title>');
});
});

View File

@@ -0,0 +1,117 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
describe('main bootstrap preview behavior', () => {
afterEach(() => {
vi.restoreAllMocks();
vi.resetModules();
});
it('starts preview server during app startup', async () => {
const mockApp = {
name: 'bDS',
whenReady: vi.fn(() => Promise.resolve()),
on: vi.fn(),
quit: vi.fn(),
};
const mockBrowserWindowGetAllWindows = vi.fn(() => [{ id: 1 }]);
class MockBrowserWindow {
static getAllWindows = mockBrowserWindowGetAllWindows;
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(),
};
}
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(),
},
}));
const mockPreviewStart = vi.fn().mockResolvedValue(4123);
const mockPreviewStop = vi.fn().mockResolvedValue(undefined);
const mockPreviewGetBaseUrl = vi.fn(() => 'http://127.0.0.1:4123');
class MockPreviewServer {
start = mockPreviewStart;
stop = mockPreviewStop;
getBaseUrl = mockPreviewGetBaseUrl;
}
vi.doMock('../../src/main/engine/PreviewServer', () => ({
PreviewServer: MockPreviewServer,
}));
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));
expect(mockPreviewStart).toHaveBeenCalledWith(4123);
expect(mockApp.whenReady).toHaveBeenCalled();
});
});