feat: preview server startup directly
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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>');
|
||||
});
|
||||
});
|
||||
117
tests/engine/mainStartup.test.ts
Normal file
117
tests/engine/mainStartup.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user