feat: preview server startup directly
This commit is contained in:
@@ -8,6 +8,8 @@ import { getProjectEngine } from './ProjectEngine';
|
|||||||
interface ActiveProjectContext {
|
interface ActiveProjectContext {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
dataDir?: string;
|
dataDir?: string;
|
||||||
|
projectName?: string;
|
||||||
|
projectDescription?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PostEngineContract {
|
interface PostEngineContract {
|
||||||
@@ -57,6 +59,30 @@ function clampMaxPostsPerPage(value: unknown): number {
|
|||||||
return normalized;
|
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 {
|
function escapeHtml(value: string): string {
|
||||||
return value
|
return value
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
@@ -162,7 +188,12 @@ export class PreviewServer {
|
|||||||
const activeProject = await projectEngine.getActiveProject();
|
const activeProject = await projectEngine.getActiveProject();
|
||||||
const projectId = activeProject?.id ?? 'default';
|
const projectId = activeProject?.id ?? 'default';
|
||||||
const dataDir = projectEngine.getDataDir(projectId, activeProject?.dataPath);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.respond(res, 200, getPageHtml(result, 'Blog Preview'));
|
this.respond(res, 200, getPageHtml(result, resolvePageTitle(metadata, context.projectName, context.projectDescription)));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[PreviewServer] Request failed:', error);
|
console.error('[PreviewServer] Request failed:', error);
|
||||||
this.respond(res, 500, 'Internal Server Error');
|
this.respond(res, 500, 'Internal Server Error');
|
||||||
|
|||||||
@@ -109,6 +109,14 @@ async function openPreviewInBrowser(): Promise<void> {
|
|||||||
await shell.openExternal(`${previewServer.getBaseUrl()}/`);
|
await shell.openExternal(`${previewServer.getBaseUrl()}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function startPreviewServerOnAppStart(): Promise<void> {
|
||||||
|
if (!previewServer) {
|
||||||
|
previewServer = new PreviewServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
await previewServer.start(PREVIEW_SERVER_PORT);
|
||||||
|
}
|
||||||
|
|
||||||
function createApplicationMenu(): Menu {
|
function createApplicationMenu(): Menu {
|
||||||
const template: MenuItemConstructorOptions[] = [
|
const template: MenuItemConstructorOptions[] = [
|
||||||
{
|
{
|
||||||
@@ -445,6 +453,11 @@ async function initialize(): Promise<void> {
|
|||||||
// App lifecycle
|
// App lifecycle
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
await initialize();
|
await initialize();
|
||||||
|
try {
|
||||||
|
await startPreviewServerOnAppStart();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start preview server on app startup:', error);
|
||||||
|
}
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
|
|||||||
@@ -275,4 +275,57 @@ describe('PreviewServer', () => {
|
|||||||
const renderedPosts = (html.match(/<div class="post">/g) || []).length;
|
const renderedPosts = (html.match(/<div class="post">/g) || []).length;
|
||||||
expect(renderedPosts).toBe(7);
|
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