import { afterEach, describe, expect, it, jest } from "@jest/globals"; import { flushPromises, mount, shallowMount } from "@vue/test-utils"; import { nextTick } from "vue"; import axios from "axios"; import App from "../../../frontend/src/App.vue"; import HeroSection from "../../../frontend/src/components/HeroSection.vue"; import MetricsPanel from "../../../frontend/src/components/MetricsPanel.vue"; import TopBar from "../../../frontend/src/components/TopBar.vue"; import { auth } from "../../../frontend/src/auth.js"; import AboutView from "../../../frontend/src/views/AboutView.vue"; import AiPlanView from "../../../frontend/src/views/AiPlanView.vue"; import AppsView from "../../../frontend/src/views/AppsView.vue"; import MoneroView from "../../../frontend/src/views/MoneroView.vue"; jest.mock("axios", () => ({ __esModule: true, default: { get: jest.fn(), }, }), { virtual: true }); jest.mock("vue-router", () => ({ RouterLink: { name: "RouterLink", props: ["to"], template: "", }, }), { virtual: true }); describe("static shell views and components", () => { afterEach(() => { jest.restoreAllMocks(); jest.useRealTimers(); document.body.style.overflow = ""; auth.enabled = false; auth.ready = false; auth.authenticated = false; auth.resetUrl = ""; }); it("renders the simple static pages", () => { const about = shallowMount(AboutView); expect(about.text()).toContain("About Me"); expect(about.text()).toContain("Titan Lab"); expect(about.text()).toContain("DevOps Automation Engineer / Senior SDET"); expect(about.text()).toContain("Kubernetes"); expect(about.text()).toContain("IBM Cloud"); expect(about.text()).toContain("TradeHat"); expect(about.find("a[href='https://www.tradehat.com/']").exists()).toBe(true); expect(about.find("a[href='https://www.unifocus.com/']").exists()).toBe(true); expect(about.find("a[href='https://scm.bstein.dev/bstein/titan-iac']").exists()).toBe(true); const apps = shallowMount(AppsView); expect(apps.text()).toContain("Apps"); expect(apps.text()).toContain("Nextcloud"); expect(apps.vm.sections.map((section) => section.title)).toContain("Security"); const appLinks = apps.vm.sections.flatMap((section) => section.groups.flatMap((group) => group.apps)); expect(appLinks.find((app) => app.name === "AI Chat")).toMatchObject({ url: "https://bstein.dev/ai/chat", target: "_blank", }); expect(appLinks.find((app) => app.name === "Monero Node")).toMatchObject({ url: "https://bstein.dev/monero", target: "_blank", }); const aiPlan = shallowMount(AiPlanView); expect(aiPlan.text()).toContain("Roadmap"); expect(aiPlan.text()).toContain("AI Image"); }); it("renders hero and metrics panels with status variants", () => { const loadingHero = shallowMount(HeroSection, { props: { title: "Atlas", subtitle: "Homelab", links: [{ label: "Metrics", href: "https://metrics.example.dev" }], loading: true, error: "", }, }); expect(loadingHero.text()).toContain("Loading live data"); const errorHero = shallowMount(HeroSection, { props: { title: "Atlas", subtitle: "Homelab", links: [], loading: false, error: "offline" }, }); expect(errorHero.text()).toContain("offline"); const connectedHero = shallowMount(HeroSection, { props: { title: "Atlas", subtitle: "Homelab", links: [], loading: false, error: "" }, }); expect(connectedHero.text()).toContain("Live data connected"); const metrics = shallowMount(MetricsPanel, { props: { metrics: { dashboard: "https://metrics.example.dev/d/atlas", description: "Live Atlas metrics", }, }, }); expect(metrics.find("iframe").attributes("src")).toBe("https://metrics.example.dev/d/atlas"); expect(metrics.text()).toContain("Live Atlas metrics"); }); it("drives top bar navigation and auth state rendering", async () => { global.fetch = jest.fn(async (resource) => { const url = typeof resource === "string" ? resource : resource?.url || ""; if (url.includes("/api/account/member-state")) { return new Response(JSON.stringify({ status: "ready", dashboard_url: "/apps", account_url: "/account" }), { status: 200, headers: { "content-type": "application/json" }, }); } return new Response("{}", { status: 200, headers: { "content-type": "application/json" } }); }); auth.enabled = true; auth.ready = true; auth.authenticated = false; auth.resetUrl = "https://sso.example.dev/reset"; const authModule = await import("../../../frontend/src/auth.js"); const logout = jest.spyOn(authModule, "logout").mockImplementation(() => {}); jest.spyOn(authModule, "login").mockImplementation(() => {}); const target = document.createElement("div"); document.body.appendChild(target); const wrapper = mount(TopBar, { attachTo: target }); try { expect(wrapper.text()).toContain("Contact"); expect(wrapper.text()).toContain("Login"); expect(wrapper.text()).toContain("Register"); expect(wrapper.text()).not.toContain("Reset Password"); expect(wrapper.find("a[href='/#contact']").exists()).toBe(true); await wrapper.find("button.menu-toggle").trigger("click"); const nav = wrapper.find("nav"); expect(nav.classes()).toContain("open"); const navItems = () => Array.from(nav.element.querySelectorAll("a[href], button:not([disabled])")); navItems()[0].focus(); nav.element.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", shiftKey: true, bubbles: true, cancelable: true })); await nextTick(); expect(document.activeElement).toBe(navItems().at(-1)); navItems().at(-1).focus(); nav.element.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true })); await nextTick(); expect(document.activeElement).toBe(navItems()[0]); nav.element.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); await nextTick(); expect(nav.classes()).not.toContain("open"); expect(document.activeElement).toBe(wrapper.find("button.menu-toggle").element); await wrapper.find("button.menu-toggle").trigger("click"); expect(nav.classes()).toContain("open"); window.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })); await nextTick(); expect(nav.classes()).not.toContain("open"); await wrapper.find("button.menu-toggle").trigger("click"); await wrapper.find("a[href='/#contact']").trigger("click"); expect(nav.classes()).not.toContain("open"); auth.authenticated = true; await flushPromises(); expect(wrapper.text()).toContain("Account"); expect(wrapper.text()).toContain("Open Services"); await wrapper.findAll("button.button").at(-1).trigger("click"); expect(logout).toHaveBeenCalled(); } finally { wrapper.unmount(); target.remove(); } }); it("loads Monero status and handles API failures", async () => { axios.get.mockResolvedValueOnce({ data: { nettype: "mainnet", status: "OK", height: 123, target_height: 125 }, }); const ok = mount(MoneroView); await flushPromises(); expect(ok.text()).toContain("mainnet"); expect(ok.text()).toContain("123"); axios.get.mockRejectedValueOnce(new Error("offline")); const failed = mount(MoneroView); await flushPromises(); expect(failed.text()).toContain("Could not reach monerod"); }); it("refreshes the app shell status and handles fetch failures", async () => { jest.useFakeTimers(); jest.spyOn(window, "setTimeout"); jest.spyOn(window, "clearTimeout"); global.fetch = jest.fn(async () => new Response(JSON.stringify({ connected: true, atlas: { up: true } }), { status: 200, headers: { "content-type": "application/json" }, }), ); const app = mount(App, { global: { stubs: { TopBar: true, "router-view": { props: ["labStatus", "loading", "error"], template: "
{{ loading }} {{ error }} {{ labStatus?.connected }}
", }, }, }, }); await flushPromises(); expect(app.text()).toContain("true"); expect(global.fetch).toHaveBeenCalledWith( expect.stringMatching(/^\/api\/lab\/status\?ts=\d+$/), expect.objectContaining({ cache: "no-store", headers: { Accept: "application/json" }, }), ); await app.unmount(); global.fetch = jest.fn(async () => { const err = new Error("aborted"); err.name = "AbortError"; throw err; }); const failed = mount(App, { global: { stubs: { TopBar: true, "router-view": { props: ["labStatus", "loading", "error"], template: "
{{ loading }} {{ error }}
", }, }, }, }); await flushPromises(); expect(failed.text()).toContain("Live data timed out"); }); });