import { afterEach, describe, expect, it, jest } from "@jest/globals"; import { flushPromises, mount, shallowMount } from "@vue/test-utils"; 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"; const mockRouterPush = jest.fn(); jest.mock("axios", () => ({ __esModule: true, default: { get: jest.fn(), }, }), { virtual: true }); jest.mock("vue-router", () => ({ RouterLink: { name: "RouterLink", props: ["to"], template: "", }, useRouter: () => ({ push: mockRouterPush }), }), { virtual: true }); describe("static shell views and components", () => { afterEach(() => { jest.restoreAllMocks(); jest.useRealTimers(); mockRouterPush.mockClear(); auth.enabled = 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.vm.skills).toContain("Kubernetes (k3s/k8s)"); 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 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 () => { auth.enabled = true; auth.authenticated = false; auth.resetUrl = "https://sso.example.dev/reset"; const login = jest.spyOn(await import("../../../frontend/src/auth.js"), "login").mockImplementation(() => {}); const logout = jest.spyOn(await import("../../../frontend/src/auth.js"), "logout").mockImplementation(() => {}); const wrapper = mount(TopBar); expect(wrapper.text()).toContain("Login"); expect(wrapper.text()).toContain("Request Access"); await wrapper.find(".profile").trigger("click"); expect(mockRouterPush).toHaveBeenCalledWith("/about"); await wrapper.find("button").trigger("click"); expect(login).toHaveBeenCalled(); auth.authenticated = true; await wrapper.vm.$nextTick(); expect(wrapper.text()).toContain("Account"); await wrapper.find("button").trigger("click"); expect(logout).toHaveBeenCalled(); }); 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"); 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"); }); });