diff --git a/build-constants.ts b/build-constants.ts new file mode 100644 index 0000000..fbf14ae --- /dev/null +++ b/build-constants.ts @@ -0,0 +1,10 @@ +import pkg from "./package.json"; +import openapiSpec from "./openapi.json"; + +// Shared `define` block for vite.config.ts and vitest.config.ts — exposes the +// UI's own package version and the ContextForge API version it was generated +// against to the client bundle (see Header.tsx). +export const versionDefines = { + __APP_VERSION__: JSON.stringify(pkg.version), + __SUPPORTED_API_VERSION__: JSON.stringify(openapiSpec.info.version), +}; diff --git a/src/components/layout/Header.test.tsx b/src/components/layout/Header.test.tsx index adfac03..a1ebb3b 100644 --- a/src/components/layout/Header.test.tsx +++ b/src/components/layout/Header.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import { Header } from "./Header"; import { useQuery } from "../../hooks/useQuery"; @@ -26,19 +27,17 @@ describe("Header", () => { ); }; - it("renders without version when useQuery returns no data", () => { + it("always shows the UI's own package version", () => { vi.mocked(useQuery).mockReturnValue({ data: null } as unknown as ReturnType); renderHeader(); expect(screen.getByTestId("quick-nav")).toBeInTheDocument(); expect(screen.getByTestId("profile-menu")).toBeInTheDocument(); - - // Check that there is no version string visible (like v1.2.3) - const versionMatch = screen.queryByText(/v\d+\.\d+\.\d+/); - expect(versionMatch).not.toBeInTheDocument(); + expect(screen.getByText(`v${__APP_VERSION__}`)).toBeInTheDocument(); }); - it("renders with version when useQuery returns version data", () => { + it("shows supported and live API versions in the hover popover", async () => { + const user = userEvent.setup(); vi.mocked(useQuery).mockReturnValue({ data: { app: { @@ -48,6 +47,19 @@ describe("Header", () => { } as unknown as ReturnType); renderHeader(); - expect(screen.getByText("v1.0.0")).toBeInTheDocument(); + await user.hover(screen.getByText(`v${__APP_VERSION__}`)); + + expect(await screen.findByText(`v${__SUPPORTED_API_VERSION__}`)).toBeInTheDocument(); + expect(await screen.findByText("v1.0.0")).toBeInTheDocument(); + }); + + it("shows the live API version as unavailable when useQuery returns no data", async () => { + const user = userEvent.setup(); + vi.mocked(useQuery).mockReturnValue({ data: null } as unknown as ReturnType); + renderHeader(); + + await user.hover(screen.getByText(`v${__APP_VERSION__}`)); + + expect(await screen.findByText("unavailable")).toBeInTheDocument(); }); }); diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index aaa9a4b..a68aab2 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -2,6 +2,8 @@ import { BookOpen } from "lucide-react"; import { useQuery } from "../../hooks/useQuery"; import { GitHubIcon } from "../icons/GitHubIcon"; import { SidebarTrigger } from "../ui/sidebar"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "../ui/hover-card"; +import { Typography } from "../ui/typography"; import { HeaderProfileMenu } from "./HeaderProfileMenu"; import { HeaderQuickNav } from "./HeaderQuickNav"; @@ -16,18 +18,39 @@ interface VersionResponse { export function Header() { const { data: versionData } = useQuery("/version?partial=false"); - const appVersion = versionData?.app?.version; + const liveApiVersion = versionData?.app?.version; return (
- {appVersion ? ( - - v{appVersion} - - ) : null} + + + + v{__APP_VERSION__} + + + +
+ + ContextForge supported API:{" "} + + v{__SUPPORTED_API_VERSION__} + + + + Live API version:{" "} + + {liveApiVersion ? `v${liveApiVersion}` : "unavailable"} + + +
+
+
) { + return ; +} + +function HoverCardTrigger({ ...props }: React.ComponentProps) { + return ; +} + +function HoverCardContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { HoverCard, HoverCardTrigger, HoverCardContent }; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..ffe7f8e 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,6 @@ /// + +// Injected by vite.config.ts `define` from package.json's version field. +declare const __APP_VERSION__: string; +// Injected by vite.config.ts `define` from openapi.json's info.version field. +declare const __SUPPORTED_API_VERSION__: string; diff --git a/vite.config.ts b/vite.config.ts index 3834e66..f18f1ff 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,11 +2,14 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import path from "path"; +import { versionDefines } from "./build-constants"; // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], + define: versionDefines, + css: { postcss: { plugins: [], diff --git a/vitest.config.ts b/vitest.config.ts index 6e0e37a..a80cd3b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,9 +4,12 @@ import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; import path from "path"; +import { versionDefines } from "./build-constants"; export default defineConfig({ plugins: [react()] as any, + // Mirrors vite.config.ts's version constants for code under test. + define: versionDefines, css: { postcss: { plugins: [],