test(bstein-home): migrate unit checks to jest

This commit is contained in:
codex 2026-04-21 06:38:05 -03:00
parent 285b00183a
commit d69669092f
16 changed files with 5491 additions and 651 deletions

View File

@ -3,5 +3,6 @@ flask-cors==4.0.0
gunicorn==21.2.0
httpx==0.27.2
PyJWT[crypto]==2.10.1
psycopg[binary]==3.2.6
# Keep the binary extra so CI runners do not need host libpq packages.
psycopg[binary]==3.2.13
psycopg-pool==3.2.6

10
frontend/babel.config.cjs Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: { node: "current" },
},
],
],
};

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@
"prebuild": "node scripts/build_media_manifest.mjs",
"build": "vite build",
"preview": "vite preview",
"test:unit": "vitest run --coverage --config ../testing/frontend/vitest.config.js",
"test:unit": "JEST_JUNIT_OUTPUT=../build/junit-frontend-unit.xml jest --ci --runInBand --config ../testing/frontend/jest.config.cjs --coverage --coverageReporters=text --coverageReporters=lcov --coverageReporters=json-summary --coverageDirectory=coverage --reporters=default --reporters=jest-junit",
"test:component": "playwright test --config ../testing/frontend/playwright-ct.config.mjs",
"test:e2e": "playwright test --config ../testing/frontend/playwright.config.mjs",
"test": "npm run test:unit && npm run test:component && npm run test:e2e",
@ -23,16 +23,21 @@
"vue-router": "^4.3.2"
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@eslint/js": "^9.22.0",
"@playwright/experimental-ct-vue": "^1.51.0",
"@playwright/test": "^1.51.0",
"@vitest/coverage-v8": "^3.0.9",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/vue3-jest": "^29.2.6",
"@vue/test-utils": "^2.4.6",
"babel-jest": "^29.7.0",
"eslint": "^9.22.0",
"globals": "^16.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-junit": "^16.0.0",
"jsdom": "^26.0.0",
"vitest": "^3.0.9",
"vite": "^5.2.0"
}
}

View File

@ -4,7 +4,7 @@ import globals from "../../frontend/node_modules/globals/index.js";
const sharedGlobals = {
...globals.browser,
...globals.node,
...globals.vitest,
...globals.jest,
};
export default [

View File

@ -0,0 +1,32 @@
const path = require("node:path");
const frontendRoot = path.resolve(__dirname, "../../frontend");
module.exports = {
rootDir: frontendRoot,
testEnvironment: "jsdom",
roots: [frontendRoot, path.resolve(__dirname, "unit")],
testMatch: ["**/*.spec.js"],
setupFilesAfterEnv: [path.resolve(__dirname, "jest.setup.js")],
moduleFileExtensions: ["js", "mjs", "json", "vue"],
transform: {
"^.+\\.vue$": "@vue/vue3-jest",
"^.+\\.[cm]?js$": "babel-jest",
},
moduleNameMapper: {
"^@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"),
},
coverageProvider: "v8",
collectCoverageFrom: [
"src/auth.js",
"src/components/MetricRow.vue",
"src/components/MermaidCard.vue",
"src/components/ServiceGrid.vue",
"src/components/StatsGrid.vue",
"src/data/sample.js",
"src/views/HomeView.vue",
],
coverageReporters: ["text", "lcov", "json-summary"],
};

View File

@ -0,0 +1,55 @@
if (!globalThis.requestIdleCallback) {
globalThis.requestIdleCallback = (callback) =>
window.setTimeout(() => callback({ didTimeout: false, timeRemaining: () => 0 }), 0);
}
if (!globalThis.cancelIdleCallback) {
globalThis.cancelIdleCallback = (handle) => window.clearTimeout(handle);
}
if (!globalThis.Headers) {
class SimpleHeaders {
constructor(init = {}) {
this.map = new Map();
const entries = init instanceof SimpleHeaders ? init.entries() : Object.entries(init || {});
for (const [key, value] of entries) {
this.set(key, value);
}
}
get(name) {
return this.map.get(String(name).toLowerCase()) ?? null;
}
set(name, value) {
this.map.set(String(name).toLowerCase(), String(value));
}
entries() {
return this.map.entries();
}
}
globalThis.Headers = SimpleHeaders;
}
if (!globalThis.Response) {
class SimpleResponse {
constructor(body = "", init = {}) {
this.body = body;
this.status = Number(init.status ?? 200);
this.ok = this.status >= 200 && this.status < 300;
this.headers = new Headers(init.headers || {});
}
async text() {
return String(this.body);
}
async json() {
return JSON.parse(String(this.body));
}
}
globalThis.Response = SimpleResponse;
}

View File

@ -0,0 +1,24 @@
export default class MockKeycloak {
constructor(config = {}) {
this.config = config;
this.authenticated = false;
this.token = "";
this.tokenParsed = {};
}
async init() {
return true;
}
async login() {
return undefined;
}
async logout() {
return undefined;
}
async updateToken() {
return true;
}
}

View File

@ -0,0 +1,10 @@
const mermaid = {
initialize() {
return undefined;
},
async render(id, diagram) {
return { svg: `<svg data-id="${id}">${diagram}</svg>` };
},
};
export default mermaid;

View File

@ -1,4 +1,32 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
const stubbedGlobals = new Map();
const vi = {
fn: (...args) => jest.fn(...args),
spyOn: (...args) => jest.spyOn(...args),
resetModules: () => jest.resetModules(),
restoreAllMocks: () => jest.restoreAllMocks(),
stubGlobal: (name, value) => {
if (!stubbedGlobals.has(name)) {
stubbedGlobals.set(name, Object.getOwnPropertyDescriptor(globalThis, name));
}
Object.defineProperty(globalThis, name, {
configurable: true,
writable: true,
value,
});
},
unstubAllGlobals: () => {
for (const [name, descriptor] of stubbedGlobals.entries()) {
if (descriptor) {
Object.defineProperty(globalThis, name, descriptor);
} else {
Reflect.deleteProperty(globalThis, name);
}
}
stubbedGlobals.clear();
},
};
async function loadAuth() {
vi.resetModules();

View File

@ -1,5 +1,5 @@
import { RouterLinkStub, shallowMount } from "../../../frontend/node_modules/@vue/test-utils/dist/vue-test-utils.esm-bundler.mjs";
import { describe, expect, it } from "../../../frontend/node_modules/vitest/dist/index.js";
import { describe, expect, it } from "@jest/globals";
import { RouterLinkStub, shallowMount } from "@vue/test-utils";
import MetricRow from "../../../frontend/src/components/MetricRow.vue";
import ServiceGrid from "../../../frontend/src/components/ServiceGrid.vue";

View File

@ -1,5 +1,5 @@
import { shallowMount } from "../../../frontend/node_modules/@vue/test-utils/dist/vue-test-utils.esm-bundler.mjs";
import { describe, expect, it } from "../../../frontend/node_modules/vitest/dist/index.js";
import { describe, expect, it } from "@jest/globals";
import { shallowMount } from "@vue/test-utils";
import HomeView from "../../../frontend/src/views/HomeView.vue";

View File

@ -0,0 +1,88 @@
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import { flushPromises, mount } from "@vue/test-utils";
import MermaidCard from "../../../frontend/src/components/MermaidCard.vue";
import mermaid from "mermaid";
describe("MermaidCard", () => {
afterEach(() => {
jest.restoreAllMocks();
document.body.style.overflow = "";
});
it("renders diagrams and handles the fullscreen overlay lifecycle", async () => {
const initSpy = jest.spyOn(mermaid, "initialize");
const renderSpy = jest.spyOn(mermaid, "render").mockResolvedValue({
svg: "<svg><g>ok</g></svg>",
});
const wrapper = mount(MermaidCard, {
props: {
title: "Network",
description: "Topology",
diagram: "graph TD;A-->B;",
cardId: "network-card",
},
});
await flushPromises();
expect(initSpy).toHaveBeenCalledTimes(1);
expect(renderSpy).toHaveBeenCalled();
expect(wrapper.html()).toContain("<svg");
await wrapper.find(".diagram").trigger("click");
expect(wrapper.find(".overlay").exists()).toBe(true);
expect(document.body.style.overflow).toBe("hidden");
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
await flushPromises();
expect(wrapper.find(".overlay").exists()).toBe(false);
expect(document.body.style.overflow).toBe("");
await wrapper.unmount();
});
it("renders Mermaid errors and re-renders when the diagram changes", async () => {
const renderSpy = jest
.spyOn(mermaid, "render")
.mockRejectedValueOnce(new Error("invalid diagram"))
.mockResolvedValueOnce({ svg: "<svg><text>recovered</text></svg>" });
const wrapper = mount(MermaidCard, {
props: {
title: "Pipeline",
description: "Initial",
diagram: "graph TD;BROKEN",
},
});
await flushPromises();
expect(wrapper.text()).toContain("Mermaid render error");
await wrapper.setProps({ diagram: "graph TD;X-->Y;" });
await flushPromises();
expect(renderSpy).toHaveBeenCalledTimes(2);
expect(wrapper.html()).toContain("<svg");
await wrapper.unmount();
expect(document.body.style.overflow).toBe("");
});
it("no-ops rendering when no diagram is provided", async () => {
const renderSpy = jest.spyOn(mermaid, "render");
const wrapper = mount(MermaidCard, {
props: {
title: "Empty",
description: "No diagram yet",
diagram: "",
},
});
await flushPromises();
expect(renderSpy).not.toHaveBeenCalled();
await wrapper.unmount();
});
});

View File

@ -1,4 +1,4 @@
import { describe, expect, it } from "../../../frontend/node_modules/vitest/dist/index.js";
import { describe, expect, it } from "@jest/globals";
import {
buildHardwareDiagram,

View File

@ -1,44 +0,0 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "../../frontend/node_modules/vitest/dist/config.js";
import vue from "../../frontend/node_modules/@vitejs/plugin-vue/dist/index.mjs";
const testingDir = path.dirname(fileURLToPath(import.meta.url));
const frontendRoot = path.resolve(testingDir, "../../frontend");
export default defineConfig({
root: frontendRoot,
plugins: [vue()],
resolve: {
alias: {
"@": path.resolve(frontendRoot, "src"),
},
},
test: {
environment: "jsdom",
globals: true,
include: [path.resolve(testingDir, "unit/**/*.spec.js")],
setupFiles: [path.resolve(testingDir, "vitest.setup.js")],
reporters: ["default", "junit"],
outputFile: {
junit: path.resolve(testingDir, "../../build/junit-frontend-unit.xml"),
},
coverage: {
provider: "v8",
reporter: ["text", "lcov", "json-summary"],
include: [
"src/auth.js",
"src/data/sample.js",
"src/components/MetricRow.vue",
"src/components/MermaidCard.vue",
"src/components/ServiceGrid.vue",
"src/components/StatsGrid.vue",
"src/views/HomeView.vue",
],
thresholds: {
lines: 95,
statements: 95,
},
},
},
});

View File

@ -1,8 +0,0 @@
if (!globalThis.requestIdleCallback) {
globalThis.requestIdleCallback = (callback) =>
window.setTimeout(() => callback({ didTimeout: false, timeRemaining: () => 0 }), 0);
}
if (!globalThis.cancelIdleCallback) {
globalThis.cancelIdleCallback = (handle) => window.clearTimeout(handle);
}