test(bstein-home): migrate unit checks to jest
This commit is contained in:
parent
285b00183a
commit
d69669092f
@ -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
10
frontend/babel.config.cjs
Normal file
@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
targets: { node: "current" },
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
5815
frontend/package-lock.json
generated
5815
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 [
|
||||
|
||||
32
testing/frontend/jest.config.cjs
Normal file
32
testing/frontend/jest.config.cjs
Normal 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"],
|
||||
};
|
||||
55
testing/frontend/jest.setup.js
Normal file
55
testing/frontend/jest.setup.js
Normal 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;
|
||||
}
|
||||
24
testing/frontend/mocks/keycloak-js.js
Normal file
24
testing/frontend/mocks/keycloak-js.js
Normal 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;
|
||||
}
|
||||
}
|
||||
10
testing/frontend/mocks/mermaid.js
Normal file
10
testing/frontend/mocks/mermaid.js
Normal 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;
|
||||
@ -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();
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
88
testing/frontend/unit/mermaid-card.spec.js
Normal file
88
testing/frontend/unit/mermaid-card.spec.js
Normal 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();
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user