Files
fusion/packages/dashboard/src/__tests__/dev-server-manager.test.ts
Fusion c8203210c2 feat(FN-2170): add dev-server preview detection and log viewer flow
- Add dev-server port detection utilities and wire detected preview state into process/manager lifecycle
- Normalize dev-server status API responses with preview URL, detected port, and manual override compatibility
- Introduce useDevServerLogs and DevServerLogViewer, and integrate them into the DevServer dashboard view
- Expand dashboard test coverage for detection edges, process/routes behavior, log history handling, and CSS regressions
2026-04-20 15:37:32 -07:00

174 lines
5.4 KiB
TypeScript

// @vitest-environment node
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
DevServerManager,
resetDevServerManager,
type DevServerUrlDetectedEvent,
type DevServerManagerOptions,
} from "../dev-server-manager.js";
import { DevServerStore } from "../dev-server-store.js";
async function createManager(
rootDir: string,
options?: DevServerManagerOptions,
): Promise<{ manager: DevServerManager; store: DevServerStore }> {
const store = new DevServerStore(rootDir);
await store.load();
const manager = new DevServerManager(rootDir, store, {
logLimit: 50,
...options,
});
await manager.initialize();
return { manager, store };
}
async function waitFor(predicate: () => boolean, timeoutMs = 5_000): Promise<void> {
const start = Date.now();
while (!predicate()) {
if (Date.now() - start > timeoutMs) {
throw new Error("Timed out waiting for condition");
}
await new Promise((resolve) => setTimeout(resolve, 25));
}
}
describe("DevServerManager", () => {
const managers: DevServerManager[] = [];
afterEach(async () => {
for (const manager of managers.splice(0)) {
try {
await manager.shutdown();
} catch {
// ignore cleanup failures
}
}
resetDevServerManager();
});
it("emits url-detected with source and detectedAt payload", async () => {
const root = await mkdtemp(join(tmpdir(), "dev-server-manager-"));
const { manager } = await createManager(root);
managers.push(manager);
const detectedPromise = new Promise<DevServerUrlDetectedEvent>((resolve) => {
manager.once("url-detected", (payload: DevServerUrlDetectedEvent) => resolve(payload));
});
await manager.start({
command: "node -e \"console.log('preview at http://localhost:5173/'); setInterval(() => {}, 1000)\"",
scriptName: "dev",
});
const detected = await detectedPromise;
expect(detected).toMatchObject({
url: "http://localhost:5173",
port: 5173,
source: "generic-url",
});
expect(Number.isNaN(Date.parse(detected.detectedAt))).toBe(false);
});
it("keeps manual preview URL effective while detected fields keep updating", async () => {
const root = await mkdtemp(join(tmpdir(), "dev-server-manager-"));
const { manager, store } = await createManager(root);
managers.push(manager);
await store.updateState({ manualUrl: "https://localhost:9999" });
await manager.start({
command: "node -e \"console.log('ready at http://localhost:4321/'); setInterval(() => {}, 1000)\"",
scriptName: "dev",
});
await waitFor(() => (store.getState().detectedPort ?? 0) === 4321);
const persisted = store.getState();
const derived = manager.getState();
expect(persisted.detectedUrl).toBe("http://localhost:4321");
expect(persisted.detectedPort).toBe(4321);
expect(persisted.manualUrl).toBe("https://localhost:9999");
expect(derived.previewUrl).toBe("https://localhost:9999");
expect(derived.previewPort).toBe(9999);
});
it("schedules and clears fallback probe timers through start/stop", async () => {
const root = await mkdtemp(join(tmpdir(), "dev-server-manager-"));
const { manager } = await createManager(root, {
processOptions: { probeDelayMs: 2_000, probeTimeoutMs: 5 },
});
managers.push(manager);
await manager.start({
command: "node -e \"setInterval(() => {}, 1000)\"",
scriptName: "dev",
});
expect(manager.hasPendingFallbackProbeTimer()).toBe(true);
await manager.stop();
expect(manager.hasPendingFallbackProbeTimer()).toBe(false);
});
it("runs fallback probe after grace period when logs do not announce a URL", async () => {
const root = await mkdtemp(join(tmpdir(), "dev-server-manager-"));
const { manager } = await createManager(root, {
processOptions: { probeDelayMs: 25, probeTimeoutMs: 5 },
});
managers.push(manager);
await manager.start({
command: "node -e \"setInterval(() => {}, 1000)\"",
scriptName: "dev",
});
expect(manager.hasPendingFallbackProbeTimer()).toBe(true);
await waitFor(() => manager.hasPendingFallbackProbeTimer() === false, 3_000);
});
it("restart keeps fallback probing active with a fresh timer", async () => {
const root = await mkdtemp(join(tmpdir(), "dev-server-manager-"));
const { manager } = await createManager(root, {
processOptions: { probeDelayMs: 2_000, probeTimeoutMs: 5 },
});
managers.push(manager);
await manager.start({
command: "node -e \"setInterval(() => {}, 1000)\"",
scriptName: "dev",
});
expect(manager.hasPendingFallbackProbeTimer()).toBe(true);
await manager.restart();
expect(manager.hasPendingFallbackProbeTimer()).toBe(true);
});
it("shutdown clears fallback probe timers", async () => {
const root = await mkdtemp(join(tmpdir(), "dev-server-manager-"));
const { manager } = await createManager(root, {
processOptions: { probeDelayMs: 2_000, probeTimeoutMs: 5 },
});
managers.push(manager);
await manager.start({
command: "node -e \"setInterval(() => {}, 1000)\"",
scriptName: "dev",
});
expect(manager.hasPendingFallbackProbeTimer()).toBe(true);
await manager.shutdown();
expect(manager.hasPendingFallbackProbeTimer()).toBe(false);
});
});