test(bstein-home): cover frontend static shell
This commit is contained in:
parent
1ca83c7499
commit
a66913950c
@ -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"),
|
||||
|
||||
1
testing/frontend/mocks/file.js
Normal file
1
testing/frontend/mocks/file.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = "test-file-stub";
|
||||
178
testing/frontend/unit/static-views.spec.js
Normal file
178
testing/frontend/unit/static-views.spec.js
Normal file
@ -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: "<a><slot /></a>",
|
||||
},
|
||||
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: "<div class=\"route-stub\">{{ loading }} {{ error }} {{ labStatus?.connected }}</div>",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
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: "<div class=\"route-stub\">{{ loading }} {{ error }}</div>",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
expect(failed.text()).toContain("Live data timed out");
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user