diff --git a/pi-web-plugins/updates/pi-web-plugin.test.ts b/pi-web-plugins/updates/pi-web-plugin.test.ts deleted file mode 100644 index a8c4043..0000000 --- a/pi-web-plugins/updates/pi-web-plugin.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { html, svg, type TemplateResult } from "lit"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { PiWebComponentStatus, PiWebStatusResponse, PluginRuntimeState, WorkspacePanelContext, WorkspacePanelContribution, WorkspacePanelTerminal } from "@jmfederico/pi-web/plugin-api"; -import plugin from "./pi-web-plugin"; - -function isTemplateResult(value: unknown): value is TemplateResult { - return typeof value === "object" && value !== null && "_$litType$" in value; -} - -function isFunction(value: unknown): value is () => void { - return typeof value === "function"; -} - -// The repo runs vitest in plain Node (no DOM), so instead of mounting the -// template we walk the lit TemplateResult tree to collect its rendered text -// and any event-handler functions. -function walk(node: unknown, text: string[], handlers: (() => void)[]): void { - if (isFunction(node)) { - handlers.push(node); - return; - } - if (Array.isArray(node)) { - for (const child of node) walk(child, text, handlers); - return; - } - if (isTemplateResult(node)) { - for (const piece of node.strings) text.push(piece); - for (const value of node.values) walk(value, text, handlers); - return; - } - if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") { - text.push(String(node)); - } -} - -function rendered(template: TemplateResult): { text: string; handlers: (() => void)[] } { - const text: string[] = []; - const handlers: (() => void)[] = []; - walk(template, text, handlers); - return { text: text.join(" "), handlers }; -} - -function badgeText(value: string | number | TemplateResult | undefined): string { - if (value === undefined) return ""; - if (isTemplateResult(value)) return rendered(value).text; - return String(value); -} - -function updatesPanel(): WorkspacePanelContribution { - const result = plugin.activate({ apiVersion: 1, pluginId: "updates", html, svg }); - const panel = result.contributions.workspacePanels?.[0]; - if (panel === undefined) throw new Error("Updates plugin did not contribute a workspace panel"); - return panel; -} - -function component(overrides: Partial = {}): PiWebComponentStatus { - return { - component: "web", - label: "Web/UI", - runtimeVersion: "1.202605.8", - installedVersion: "1.202605.8", - stale: false, - available: true, - ...overrides, - }; -} - -function status(overrides: Partial = {}): PiWebStatusResponse { - return { - packageName: "@jmfederico/pi-web", - generatedAt: "2026-06-14T00:00:00.000Z", - components: { - web: component({ component: "web", label: "Web/UI" }), - sessiond: component({ component: "sessiond", label: "Session daemon" }), - }, - release: { packageName: "@jmfederico/pi-web", updateAvailable: false }, - commands: {}, - messages: [], - ...overrides, - }; -} - -function noopTerminal(): WorkspacePanelTerminal { - return { open: vi.fn(), runCommand: vi.fn().mockResolvedValue({}) }; -} - -function contextFor(state: PluginRuntimeState | undefined, terminal: WorkspacePanelTerminal = noopTerminal()): WorkspacePanelContext { - return { - machine: { id: "local", name: "Local", kind: "local" }, - workspace: { id: "ws", projectId: "p", path: "/tmp/ws", label: "ws", isMain: true, isGitRepo: false, isGitWorktree: false }, - ...(state === undefined ? {} : { state }), - files: { readFile: vi.fn() }, - host: { requestRender: vi.fn() }, - terminal, - }; -} - -describe("Updates plugin panel", () => { - it("contributes a single Updates workspace panel", () => { - const panel = updatesPanel(); - expect(panel.id).toBe("workspace.updates"); - expect(panel.title).toBe("Updates"); - }); - - it("shows a checking placeholder before status is available", () => { - const { text } = rendered(updatesPanel().render(contextFor(undefined))); - expect(text).toContain("Checking PI WEB update status"); - }); - - it("renders installed services and commands without throwing", () => { - const value = status({ - release: { packageName: "@jmfederico/pi-web", updateAvailable: true }, - commands: { update: "pi-web update && pi-web restart", status: "pi-web status" }, - }); - const { text } = rendered(updatesPanel().render(contextFor({ piWebStatus: value }))); - expect(text).toContain("Installed services"); - expect(text).toContain("Recommended"); - expect(text).toContain("Copy"); - }); - - describe("with a terminal", () => { - const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, "navigator"); - - beforeEach(() => { - Object.defineProperty(globalThis, "navigator", { - value: { clipboard: { writeText: vi.fn() } }, - configurable: true, - }); - }); - - afterEach(() => { - if (originalNavigator === undefined) Reflect.deleteProperty(globalThis, "navigator"); - else Object.defineProperty(globalThis, "navigator", originalNavigator); - }); - - it("renders Run actions and wires them to the terminal with plugin metadata", () => { - const runCommand = vi.fn().mockResolvedValue({}); - const terminal: WorkspacePanelTerminal = { open: vi.fn(), runCommand }; - const value = status({ - release: { packageName: "@jmfederico/pi-web", updateAvailable: true }, - commands: { update: "pi-web update && pi-web restart" }, - }); - - const { text, handlers } = rendered(updatesPanel().render(contextFor({ piWebStatus: value }, terminal))); - expect(text).toContain(">Run<"); - - for (const handler of handlers) handler(); - - expect(runCommand).toHaveBeenCalledWith(expect.objectContaining({ - command: "pi-web update && pi-web restart", - open: true, - metadata: { "pi.plugin": "updates" }, - })); - }); - }); - - describe("visibility and badge", () => { - it("is hidden for managed installs with no messages and visible for local installs", () => { - const panel = updatesPanel(); - const managed = status({ - components: { - web: component({ installation: { kind: "pi-package" } }), - sessiond: component({ component: "sessiond", label: "Session daemon", installation: { kind: "npm-global" } }), - }, - }); - const local = status({ - components: { - web: component({ installation: { kind: "local" } }), - sessiond: component({ component: "sessiond", label: "Session daemon", installation: { kind: "pi-package" } }), - }, - }); - expect(panel.visible?.(contextFor({ piWebStatus: managed }))).toBe(false); - expect(panel.visible?.(contextFor({ piWebStatus: local }))).toBe(true); - }); - - it("marks the badge beta and appends the message count", () => { - const panel = updatesPanel(); - expect(badgeText(panel.badge?.(contextFor({ piWebStatus: status() })))).toContain("beta"); - - const withMessages = badgeText(panel.badge?.(contextFor({ piWebStatus: status({ messages: [{ id: "a", severity: "warning", title: "t", body: "b" }] }) }))); - expect(withMessages).toContain("beta"); - expect(withMessages).toContain("1"); - }); - }); -});