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
|
gunicorn==21.2.0
|
||||||
httpx==0.27.2
|
httpx==0.27.2
|
||||||
PyJWT[crypto]==2.10.1
|
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
|
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",
|
"prebuild": "node scripts/build_media_manifest.mjs",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"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:component": "playwright test --config ../testing/frontend/playwright-ct.config.mjs",
|
||||||
"test:e2e": "playwright test --config ../testing/frontend/playwright.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",
|
"test": "npm run test:unit && npm run test:component && npm run test:e2e",
|
||||||
@ -23,16 +23,21 @@
|
|||||||
"vue-router": "^4.3.2"
|
"vue-router": "^4.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.26.0",
|
||||||
|
"@babel/preset-env": "^7.26.0",
|
||||||
"@eslint/js": "^9.22.0",
|
"@eslint/js": "^9.22.0",
|
||||||
"@playwright/experimental-ct-vue": "^1.51.0",
|
"@playwright/experimental-ct-vue": "^1.51.0",
|
||||||
"@playwright/test": "^1.51.0",
|
"@playwright/test": "^1.51.0",
|
||||||
"@vitest/coverage-v8": "^3.0.9",
|
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
|
"@vue/vue3-jest": "^29.2.6",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
|
"babel-jest": "^29.7.0",
|
||||||
"eslint": "^9.22.0",
|
"eslint": "^9.22.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
"jest-junit": "^16.0.0",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"vitest": "^3.0.9",
|
|
||||||
"vite": "^5.2.0"
|
"vite": "^5.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import globals from "../../frontend/node_modules/globals/index.js";
|
|||||||
const sharedGlobals = {
|
const sharedGlobals = {
|
||||||
...globals.browser,
|
...globals.browser,
|
||||||
...globals.node,
|
...globals.node,
|
||||||
...globals.vitest,
|
...globals.jest,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default [
|
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() {
|
async function loadAuth() {
|
||||||
vi.resetModules();
|
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 "@jest/globals";
|
||||||
import { describe, expect, it } from "../../../frontend/node_modules/vitest/dist/index.js";
|
import { RouterLinkStub, shallowMount } from "@vue/test-utils";
|
||||||
|
|
||||||
import MetricRow from "../../../frontend/src/components/MetricRow.vue";
|
import MetricRow from "../../../frontend/src/components/MetricRow.vue";
|
||||||
import ServiceGrid from "../../../frontend/src/components/ServiceGrid.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 "@jest/globals";
|
||||||
import { describe, expect, it } from "../../../frontend/node_modules/vitest/dist/index.js";
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
|
||||||
import HomeView from "../../../frontend/src/views/HomeView.vue";
|
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 {
|
import {
|
||||||
buildHardwareDiagram,
|
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