feat(FN-2186): merge fusion/fn-2186
This commit is contained in:
@@ -113,6 +113,36 @@ describe("devserver-detect", () => {
|
|||||||
expect(detected).toEqual([]);
|
expect(detected).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns empty array for malformed package.json without throwing", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "devserver-detect-"));
|
||||||
|
const filePath = join(root, "package.json");
|
||||||
|
await mkdir(dirname(filePath), { recursive: true });
|
||||||
|
await writeFile(filePath, '{ "invalid json', "utf-8");
|
||||||
|
|
||||||
|
const detected = await detectDevServerCommands(root);
|
||||||
|
expect(detected).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deeply nested package.json (2+ levels) is NOT scanned", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "devserver-detect-"));
|
||||||
|
await writePackageJson(root, { scripts: {} });
|
||||||
|
// Create a deeply nested package.json (2 levels deep)
|
||||||
|
await writePackageJson(
|
||||||
|
root,
|
||||||
|
{
|
||||||
|
scripts: {
|
||||||
|
dev: "vite",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"apps/web/src/package.json",
|
||||||
|
);
|
||||||
|
|
||||||
|
const detected = await detectDevServerCommands(root);
|
||||||
|
|
||||||
|
// Should not find the deeply nested package.json
|
||||||
|
expect(detected).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
it("persists and reloads devserver configs", async () => {
|
it("persists and reloads devserver configs", async () => {
|
||||||
const root = await mkdtemp(join(tmpdir(), "devserver-config-"));
|
const root = await mkdtemp(join(tmpdir(), "devserver-config-"));
|
||||||
const configs: DevServerConfig[] = [
|
const configs: DevServerConfig[] = [
|
||||||
|
|||||||
@@ -255,6 +255,32 @@ describe("devserver-manager", () => {
|
|||||||
expect(restarted.config.id).toBe(config.id);
|
expect(restarted.config.id).toBe(config.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("startServer throws error when process emits error event", async () => {
|
||||||
|
const config = makeConfig();
|
||||||
|
const onStatus = vi.fn();
|
||||||
|
manager.on("status", onStatus);
|
||||||
|
|
||||||
|
await manager.startServer(config);
|
||||||
|
|
||||||
|
// Simulate process error (e.g., command not found)
|
||||||
|
children[0].emit("error", new Error("spawn ENOENT"));
|
||||||
|
|
||||||
|
const session = manager.getSession(config.id);
|
||||||
|
expect(session?.status).toBe("failed");
|
||||||
|
expect(onStatus).toHaveBeenLastCalledWith(config.id, "failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("startServer throws error when same server is already running", async () => {
|
||||||
|
const config = makeConfig();
|
||||||
|
await manager.startServer(config);
|
||||||
|
children[0].stdout.emit("data", "ready");
|
||||||
|
|
||||||
|
// Try to start the same server again
|
||||||
|
await expect(manager.startServer(config)).rejects.toThrow(
|
||||||
|
/already (?:running|starting)/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("setPreviewUrl updates session and emits preview event", async () => {
|
it("setPreviewUrl updates session and emits preview event", async () => {
|
||||||
const config = makeConfig();
|
const config = makeConfig();
|
||||||
const onPreview = vi.fn();
|
const onPreview = vi.fn();
|
||||||
|
|||||||
164
packages/dashboard/src/__tests__/devserver-persistence.test.ts
Normal file
164
packages/dashboard/src/__tests__/devserver-persistence.test.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
// @vitest-environment node
|
||||||
|
|
||||||
|
import { mkdir, writeFile } from "node:fs/promises";
|
||||||
|
import { mkdtemp } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { loadDevServerConfigs, saveDevServerConfigs } from "../devserver-persistence.js";
|
||||||
|
import { createDevServerId, type DevServerConfig } from "../devserver-types.js";
|
||||||
|
|
||||||
|
describe("devserver-persistence", () => {
|
||||||
|
it("saves and reloads multiple dev server configs", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "devserver-persist-"));
|
||||||
|
const configs: DevServerConfig[] = [
|
||||||
|
{
|
||||||
|
id: createDevServerId("server-1"),
|
||||||
|
name: "Frontend",
|
||||||
|
command: "npm run dev",
|
||||||
|
cwd: root,
|
||||||
|
env: { NODE_ENV: "development" },
|
||||||
|
autoStart: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createDevServerId("server-2"),
|
||||||
|
name: "Storybook",
|
||||||
|
command: "npm run storybook",
|
||||||
|
cwd: root,
|
||||||
|
env: { STORYBOOK_MODE: "static" },
|
||||||
|
autoStart: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await saveDevServerConfigs(root, configs);
|
||||||
|
const loaded = await loadDevServerConfigs(root);
|
||||||
|
|
||||||
|
expect(loaded).toHaveLength(2);
|
||||||
|
expect(loaded[0]?.id).toBe("server-1");
|
||||||
|
expect(loaded[0]?.name).toBe("Frontend");
|
||||||
|
expect(loaded[0]?.command).toBe("npm run dev");
|
||||||
|
expect(loaded[0]?.cwd).toBe(root);
|
||||||
|
expect(loaded[0]?.env).toEqual({ NODE_ENV: "development" });
|
||||||
|
expect(loaded[0]?.autoStart).toBe(true);
|
||||||
|
expect(loaded[1]?.id).toBe("server-2");
|
||||||
|
expect(loaded[1]?.name).toBe("Storybook");
|
||||||
|
expect(loaded[1]?.autoStart).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves exact config properties on reload", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "devserver-persist-exact-"));
|
||||||
|
const config: DevServerConfig = {
|
||||||
|
id: createDevServerId("exact-test"),
|
||||||
|
name: "Exact Config",
|
||||||
|
command: "pnpm dev --port 3000",
|
||||||
|
cwd: "/custom/path",
|
||||||
|
env: {
|
||||||
|
CUSTOM_VAR: "value1",
|
||||||
|
ANOTHER_VAR: "value2",
|
||||||
|
},
|
||||||
|
autoStart: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await saveDevServerConfigs(root, [config]);
|
||||||
|
const [loaded] = await loadDevServerConfigs(root);
|
||||||
|
|
||||||
|
expect(loaded).toEqual(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty array when no configs have been saved", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "devserver-persist-empty-"));
|
||||||
|
|
||||||
|
const loaded = await loadDevServerConfigs(root);
|
||||||
|
expect(loaded).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles corrupted devserver.json gracefully", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "devserver-persist-corrupt-"));
|
||||||
|
const configPath = join(root, ".fusion", "devserver.json");
|
||||||
|
await mkdir(dirname(configPath), { recursive: true });
|
||||||
|
await writeFile(configPath, '{ "invalid json', "utf-8");
|
||||||
|
|
||||||
|
const loaded = await loadDevServerConfigs(root);
|
||||||
|
expect(loaded).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles empty devserver.json", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "devserver-persist-empty-file-"));
|
||||||
|
const configPath = join(root, ".fusion", "devserver.json");
|
||||||
|
await mkdir(dirname(configPath), { recursive: true });
|
||||||
|
await writeFile(configPath, '', "utf-8");
|
||||||
|
|
||||||
|
const loaded = await loadDevServerConfigs(root);
|
||||||
|
expect(loaded).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles devserver.json with missing configs array", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "devserver-persist-missing-"));
|
||||||
|
const configPath = join(root, ".fusion", "devserver.json");
|
||||||
|
await mkdir(dirname(configPath), { recursive: true });
|
||||||
|
await writeFile(configPath, '{"other": "data"}', "utf-8");
|
||||||
|
|
||||||
|
const loaded = await loadDevServerConfigs(root);
|
||||||
|
expect(loaded).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("persists three or more servers correctly", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "devserver-persist-multi-"));
|
||||||
|
const configs: DevServerConfig[] = [
|
||||||
|
{ id: createDevServerId("multi-1"), name: "Server 1", command: "npm run dev", cwd: root },
|
||||||
|
{ id: createDevServerId("multi-2"), name: "Server 2", command: "npm run storybook", cwd: root },
|
||||||
|
{ id: createDevServerId("multi-3"), name: "Server 3", command: "npm run start", cwd: root },
|
||||||
|
];
|
||||||
|
|
||||||
|
await saveDevServerConfigs(root, configs);
|
||||||
|
const loaded = await loadDevServerConfigs(root);
|
||||||
|
|
||||||
|
expect(loaded).toHaveLength(3);
|
||||||
|
expect(loaded.map((c) => c.id)).toEqual(["multi-1", "multi-2", "multi-3"]);
|
||||||
|
expect(loaded.map((c) => c.name)).toEqual(["Server 1", "Server 2", "Server 3"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters out invalid configs during load", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "devserver-persist-filter-"));
|
||||||
|
const configPath = join(root, ".fusion", "devserver.json");
|
||||||
|
await mkdir(dirname(configPath), { recursive: true });
|
||||||
|
// Write manually with an invalid config (missing required fields)
|
||||||
|
await writeFile(
|
||||||
|
configPath,
|
||||||
|
JSON.stringify({
|
||||||
|
configs: [
|
||||||
|
{ id: "valid-1", name: "Valid", command: "npm run dev", cwd: root },
|
||||||
|
{ id: "invalid-1" }, // Missing required fields
|
||||||
|
{ name: "Also invalid" }, // Missing id
|
||||||
|
{ id: "valid-2", name: "Also Valid", command: "npm run start", cwd: root },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const loaded = await loadDevServerConfigs(root);
|
||||||
|
|
||||||
|
expect(loaded).toHaveLength(2);
|
||||||
|
expect(loaded[0]?.id).toBe("valid-1");
|
||||||
|
expect(loaded[1]?.id).toBe("valid-2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overwrites existing configs when saving", async () => {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "devserver-persist-overwrite-"));
|
||||||
|
|
||||||
|
const initialConfigs: DevServerConfig[] = [
|
||||||
|
{ id: createDevServerId("overwrite-1"), name: "Initial", command: "npm run dev", cwd: root },
|
||||||
|
];
|
||||||
|
await saveDevServerConfigs(root, initialConfigs);
|
||||||
|
|
||||||
|
const overwrittenConfigs: DevServerConfig[] = [
|
||||||
|
{ id: createDevServerId("overwrite-2"), name: "New Config", command: "npm run start", cwd: root },
|
||||||
|
];
|
||||||
|
await saveDevServerConfigs(root, overwrittenConfigs);
|
||||||
|
|
||||||
|
const loaded = await loadDevServerConfigs(root);
|
||||||
|
expect(loaded).toHaveLength(1);
|
||||||
|
expect(loaded[0]?.id).toBe("overwrite-2");
|
||||||
|
expect(loaded[0]?.name).toBe("New Config");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
// @vitest-environment node
|
||||||
|
|
||||||
|
import { EventEmitter } from "node:events";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { DevServerManager, destroyAllDevServerManagers } from "../devserver-manager.js";
|
||||||
|
import { createDevServerId, type DevServerConfig } from "../devserver-types.js";
|
||||||
|
|
||||||
|
const { spawnMock, createConnectionMock } = vi.hoisted(() => ({
|
||||||
|
spawnMock: vi.fn(),
|
||||||
|
createConnectionMock: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("node:child_process", () => ({
|
||||||
|
spawn: spawnMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("node:net", () => ({
|
||||||
|
createConnection: createConnectionMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface MockSocket extends EventEmitter {
|
||||||
|
setTimeout: (ms: number) => void;
|
||||||
|
destroy: () => void;
|
||||||
|
end: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MockChildProcess extends EventEmitter {
|
||||||
|
pid: number;
|
||||||
|
killed: boolean;
|
||||||
|
stdout: EventEmitter;
|
||||||
|
stderr: EventEmitter;
|
||||||
|
kill: ReturnType<typeof vi.fn>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockSocket(): MockSocket {
|
||||||
|
const socket = new EventEmitter() as MockSocket;
|
||||||
|
socket.setTimeout = vi.fn();
|
||||||
|
socket.destroy = vi.fn();
|
||||||
|
socket.end = vi.fn();
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockChildProcess(pid: number): MockChildProcess {
|
||||||
|
const child = new EventEmitter() as MockChildProcess;
|
||||||
|
child.pid = pid;
|
||||||
|
child.killed = false;
|
||||||
|
child.stdout = new EventEmitter();
|
||||||
|
child.stderr = new EventEmitter();
|
||||||
|
child.kill = vi.fn(() => {
|
||||||
|
child.killed = true;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeConfig(id = "preview-test"): DevServerConfig {
|
||||||
|
return {
|
||||||
|
id: createDevServerId(id),
|
||||||
|
name: id,
|
||||||
|
command: "npm run dev",
|
||||||
|
cwd: "/repo",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("devserver-preview-detect", () => {
|
||||||
|
let manager: DevServerManager;
|
||||||
|
let nextPid: number;
|
||||||
|
let children: MockChildProcess[];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
nextPid = 1000;
|
||||||
|
children = [];
|
||||||
|
spawnMock.mockReset();
|
||||||
|
createConnectionMock.mockReset();
|
||||||
|
|
||||||
|
spawnMock.mockImplementation(() => {
|
||||||
|
const child = createMockChildProcess(nextPid++);
|
||||||
|
children.push(child);
|
||||||
|
return child;
|
||||||
|
});
|
||||||
|
|
||||||
|
createConnectionMock.mockImplementation(() => {
|
||||||
|
const socket = createMockSocket();
|
||||||
|
queueMicrotask(() => {
|
||||||
|
socket.emit("error", new Error("closed"));
|
||||||
|
});
|
||||||
|
return socket;
|
||||||
|
});
|
||||||
|
|
||||||
|
manager = new DevServerManager("/project");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
manager.destroy();
|
||||||
|
destroyAllDevServerManagers();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects URL from stdout with localhost:3000", async () => {
|
||||||
|
const config = makeConfig();
|
||||||
|
await manager.startServer(config);
|
||||||
|
children[0].stdout.emit("data", "Server ready at http://localhost:3000");
|
||||||
|
|
||||||
|
expect(manager.getSession(config.id)?.previewUrl).toBe("http://localhost:3000");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects URL from stdout with 127.0.0.1:5173", async () => {
|
||||||
|
const config = makeConfig();
|
||||||
|
await manager.startServer(config);
|
||||||
|
children[0].stdout.emit("data", "Vite ready: http://127.0.0.1:5173");
|
||||||
|
|
||||||
|
expect(manager.getSession(config.id)?.previewUrl).toBe("http://127.0.0.1:5173");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects URL from stderr output", async () => {
|
||||||
|
const config = makeConfig();
|
||||||
|
await manager.startServer(config);
|
||||||
|
children[0].stderr.emit("data", "Listening on http://localhost:4000");
|
||||||
|
|
||||||
|
expect(manager.getSession(config.id)?.previewUrl).toBe("http://localhost:4000");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("first URL wins when multiple URLs are present", async () => {
|
||||||
|
const config = makeConfig();
|
||||||
|
await manager.startServer(config);
|
||||||
|
children[0].stdout.emit(
|
||||||
|
"data",
|
||||||
|
"Ready at http://localhost:3000 then proxy at http://localhost:3001",
|
||||||
|
);
|
||||||
|
|
||||||
|
// First URL should win
|
||||||
|
expect(manager.getSession(config.id)?.previewUrl).toBe("http://localhost:3000");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("setPreviewUrl updates the preview URL manually", async () => {
|
||||||
|
const config = makeConfig();
|
||||||
|
await manager.startServer(config);
|
||||||
|
|
||||||
|
// Set manual URL
|
||||||
|
manager.setPreviewUrl(config.id, "http://custom:9999");
|
||||||
|
|
||||||
|
expect(manager.getSession(config.id)?.previewUrl).toBe("http://custom:9999");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("port probing fallback sets URL when no URL is detected", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
createConnectionMock.mockReset();
|
||||||
|
createConnectionMock.mockImplementation((options: { port: number }) => {
|
||||||
|
const socket = createMockSocket();
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (options.port === 3000) {
|
||||||
|
socket.emit("connect");
|
||||||
|
} else {
|
||||||
|
socket.emit("error", new Error("closed"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return socket;
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = makeConfig();
|
||||||
|
await manager.startServer(config);
|
||||||
|
// No URL output, just some generic output
|
||||||
|
children[0].stdout.emit("data", "started");
|
||||||
|
|
||||||
|
// Advance timers past the probe delay (10s)
|
||||||
|
await vi.advanceTimersByTimeAsync(10_001);
|
||||||
|
|
||||||
|
expect(manager.getSession(config.id)?.previewUrl).toBe("http://localhost:3000");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("URL stays null when port probing finds nothing", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
createConnectionMock.mockReset();
|
||||||
|
// All connection attempts fail
|
||||||
|
createConnectionMock.mockImplementation(() => {
|
||||||
|
const socket = createMockSocket();
|
||||||
|
queueMicrotask(() => {
|
||||||
|
socket.emit("error", new Error("closed"));
|
||||||
|
});
|
||||||
|
return socket;
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = makeConfig();
|
||||||
|
await manager.startServer(config);
|
||||||
|
// No URL output
|
||||||
|
children[0].stdout.emit("data", "started");
|
||||||
|
|
||||||
|
// Advance timers past the probe delay
|
||||||
|
await vi.advanceTimersByTimeAsync(10_001);
|
||||||
|
|
||||||
|
expect(manager.getSession(config.id)?.previewUrl).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clearing manual override reveals auto-detected URL", async () => {
|
||||||
|
const config = makeConfig();
|
||||||
|
await manager.startServer(config);
|
||||||
|
|
||||||
|
// Set manual override
|
||||||
|
manager.setPreviewUrl(config.id, "http://custom:9999");
|
||||||
|
|
||||||
|
// Then clear it
|
||||||
|
manager.setPreviewUrl(config.id, null);
|
||||||
|
|
||||||
|
// Should be undefined now (since no auto-detection happened)
|
||||||
|
expect(manager.getSession(config.id)?.previewUrl).toBeUndefined();
|
||||||
|
|
||||||
|
// Now emit auto-detection
|
||||||
|
children[0].stdout.emit("data", "Server at http://localhost:3000");
|
||||||
|
|
||||||
|
expect(manager.getSession(config.id)?.previewUrl).toBe("http://localhost:3000");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits preview event on URL detection", async () => {
|
||||||
|
const config = makeConfig();
|
||||||
|
const onPreview = vi.fn();
|
||||||
|
manager.on("preview", onPreview);
|
||||||
|
|
||||||
|
await manager.startServer(config);
|
||||||
|
children[0].stdout.emit("data", "Ready at http://localhost:3000");
|
||||||
|
|
||||||
|
expect(onPreview).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onPreview).toHaveBeenCalledWith(config.id, "http://localhost:3000");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits preview event on manual override", async () => {
|
||||||
|
const config = makeConfig();
|
||||||
|
const onPreview = vi.fn();
|
||||||
|
manager.on("preview", onPreview);
|
||||||
|
|
||||||
|
await manager.startServer(config);
|
||||||
|
manager.setPreviewUrl(config.id, "http://manual:8888");
|
||||||
|
|
||||||
|
expect(onPreview).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onPreview).toHaveBeenCalledWith(config.id, "http://manual:8888");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles URL in the middle of log line", async () => {
|
||||||
|
const config = makeConfig();
|
||||||
|
await manager.startServer(config);
|
||||||
|
children[0].stdout.emit(
|
||||||
|
"data",
|
||||||
|
"[INFO] Application started on http://localhost:5173 and ready for connections",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getSession(config.id)?.previewUrl).toBe("http://localhost:5173");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects URL with explicit port", async () => {
|
||||||
|
const config = makeConfig();
|
||||||
|
await manager.startServer(config);
|
||||||
|
children[0].stdout.emit("data", "Running at http://localhost:3000");
|
||||||
|
|
||||||
|
expect(manager.getSession(config.id)?.previewUrl).toBe("http://localhost:3000");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user