import { beforeEach, describe, expect, it } from "vitest"; import type { RunConfig } from "@/platform/ipc"; import { DEFAULT_TYPES, readRan, readTypes, runServiceItems, sessionDetail, sessionStatus } from "./services"; import { startedPort } from "./serverPort"; import type { RunSession } from "./store"; const config = (id: string, kind: string): RunConfig => ({ id, name: id, kind, command: kind, args: [], cwd: null, env: {}, detected: true, isTest: false, debugAdapter: null, }); const session = ( id: string, c: RunConfig, status: RunSession["status"], startedAt: number, extra: Partial = {}, ): RunSession => ({ id, config: c, runId: null, status, exitCode: null, startedAt, endedAt: null, stdinClosed: false, ...extra, }); describe("run configurations in Services", () => { const build = config("build", "cargo"); const test = config("test", "cargo"); const dev = config("dev", "npm"); const scratch = config("context:file", "python"); it("lists added types fully and other types only with their runs", () => { const sessions = [ session("s1", dev, "finished", 1), session("s2", dev, "running", 2), session("s3", scratch, "failed", 3, { exitCode: 2 }), ]; const items = runServiceItems([build, test, dev], sessions, ["cargo"]); expect(items.map((i) => [i.key, i.label, i.detail])).toEqual([ ["run:type:cargo", "Cargo", undefined], ["run:type:npm", "npm", "1 running"], ["run:type:python", "Python", undefined], ]); expect(items[0]!.children!.map((c) => [c.key, c.detail, c.dim])).toEqual([ ["run:config:build", undefined, true], ["run:config:test", undefined, true], ]); // The newest session of a configuration decides its state. expect(items[1]!.children!.map((c) => [c.label, c.detail, c.dim, c.icon])).toEqual([ ["dev", "Running", false, "run"], ]); expect(items[2]!.children!.map((c) => [c.key, c.detail])).toEqual([["run:config:context:file", "Failed (exit 2)"]]); }); it("has no groups without added types or sessions", () => { expect(runServiceItems([build], [], [])).toEqual([]); }); it("describes session states", () => { expect(sessionStatus(undefined)).toBeUndefined(); expect(sessionStatus(session("a", build, "starting", 0))).toBe("Starting"); expect(sessionStatus(session("a", build, "failed", 0, { stopped: true }))).toBe("Stopped"); expect(sessionStatus(session("a", build, "finished", 0))).toBe("Finished"); }); it("shows Spring Boot by default, hidden while the project has none", () => { const app = config("spring:app", "spring-boot"); expect(runServiceItems([build], [], DEFAULT_TYPES)).toEqual([]); const items = runServiceItems([build, app], [], DEFAULT_TYPES); expect(items.map((i) => [i.key, i.label, i.expanded])).toEqual([["run:type:spring-boot", "Spring Boot", true]]); expect(items[0]!.children!.map((c) => [c.label, c.detail, c.dim])).toEqual([["spring:app", undefined, true]]); }); it("shows the HTTP port of a running application instead of its state", () => { const app = config("spring:app", "spring-boot"); const running = session("s1", app, "running", 1, { httpPort: 8080 }); expect(sessionDetail(running)).toBe(":8080"); expect(sessionDetail(session("s1", app, "running", 1))).toBe("Running"); expect(sessionDetail(session("s1", app, "finished", 1, { httpPort: 8080, stopped: true }))).toBe("Stopped"); expect(runServiceItems([app], [running], DEFAULT_TYPES)[0]!.children![0]!.detail).toBe(":8080"); }); }); describe("types added to Services", () => { beforeEach(() => localStorage.clear()); it("defaults to Spring Boot and persists the user's list per project", () => { expect(readTypes("/p")).toEqual(["spring-boot"]); localStorage.setItem("openidea.services.runTypes:/p", JSON.stringify(["npm"])); expect(readTypes("/p")).toEqual(["npm"]); localStorage.setItem("openidea.services.runTypes:/p", "[]"); expect(readTypes("/p")).toEqual([]); expect(readTypes("/q")).toEqual(["spring-boot"]); }); }); describe("configurations that ran", () => { const dev = config("dev", "npm"); const scratch = config("context:file", "python"); beforeEach(() => localStorage.clear()); it("persists the ran configuration ids per project", () => { expect(readRan("/p")).toEqual([]); localStorage.setItem("openidea.services.ran:/p", JSON.stringify(["dev"])); expect(readRan("/p")).toEqual(["dev"]); expect(readRan("/q")).toEqual([]); }); it("lists previously run configurations without a session", () => { const items = runServiceItems([dev, scratch], [], [], ["dev", "context:file"]); expect(items.map((i) => [i.key, i.label])).toEqual([ ["run:type:npm", "npm"], ["run:type:python", "Python"], ]); expect(items[0]!.children!.map((c) => [c.label, c.dim])).toEqual([["dev", true]]); expect(items[1]!.children!.map((c) => c.key)).toEqual(["run:config:context:file"]); }); it("ignores ran ids whose configuration is gone and merges sessions", () => { const gone = runServiceItems([dev], [], [], ["missing"]); expect(gone).toEqual([]); const running = session("s1", dev, "running", 1); const items = runServiceItems([dev], [running], [], ["dev"]); expect(items).toHaveLength(1); expect(items[0]!.children!.map((c) => [c.label, c.detail])).toEqual([["dev", "Running"]]); }); }); describe("embedded web server port", () => { it("reads the port from the started line", () => { expect(startedPort("TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'")).toBe(8080); expect(startedPort("Tomcat started on port(s): 8081 (http) with context path ''")).toBe(8081); expect(startedPort("o.s.b.web.embedded.netty.NettyWebServer : Netty started on port 9090")).toBe(9090); expect(startedPort("Netty started on port(s): 9091")).toBe(9091); expect(startedPort("Undertow started on port(s) 8082 (http)")).toBe(8082); expect(startedPort("Jetty started on port 8083 (http/1.1) with context path '/'")).toBe(8083); expect(startedPort("Tomcat initialized with port 8080 (http)")).toBeNull(); expect(startedPort("Started DemoApplication in 1.2 seconds")).toBeNull(); expect(startedPort("Tomcat started on port 99999")).toBeNull(); }); });