From a66913950c2b4ee068c9afa07ef4b98f28005a79 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 21 Apr 2026 08:33:37 -0300 Subject: [PATCH] test(bstein-home): cover frontend static shell --- testing/frontend/jest.config.cjs | 3 + testing/frontend/mocks/file.js | 1 + testing/frontend/unit/static-views.spec.js | 178 +++++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 testing/frontend/mocks/file.js create mode 100644 testing/frontend/unit/static-views.spec.js diff --git a/testing/frontend/jest.config.cjs b/testing/frontend/jest.config.cjs index 951e41b..7ad6b7c 100644 --- a/testing/frontend/jest.config.cjs +++ b/testing/frontend/jest.config.cjs @@ -14,6 +14,9 @@ module.exports = { "^.+\\.[cm]?js$": "babel-jest", }, moduleNameMapper: { + "^@/assets/.*\\.(jpg|jpeg|png|gif|webp|svg)$": path.resolve(__dirname, "mocks/file.js"), + "\\.(jpg|jpeg|png|gif|webp|svg)$": path.resolve(__dirname, "mocks/file.js"), + "^@/(.*)$": path.resolve(frontendRoot, "src/$1"), "^@vue/test-utils$": path.resolve(frontendRoot, "node_modules/@vue/test-utils/dist/vue-test-utils.cjs.js"), "^keycloak-js$": path.resolve(__dirname, "mocks/keycloak-js.js"), "^mermaid$": path.resolve(__dirname, "mocks/mermaid.js"), diff --git a/testing/frontend/mocks/file.js b/testing/frontend/mocks/file.js new file mode 100644 index 0000000..0a445d0 --- /dev/null +++ b/testing/frontend/mocks/file.js @@ -0,0 +1 @@ +module.exports = "test-file-stub"; diff --git a/testing/frontend/unit/static-views.spec.js b/testing/frontend/unit/static-views.spec.js new file mode 100644 index 0000000..5b059bd --- /dev/null +++ b/testing/frontend/unit/static-views.spec.js @@ -0,0 +1,178 @@ +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"); + }); +});