test(bstein-home): cover ai and frontend entry

This commit is contained in:
codex 2026-04-21 09:00:20 -03:00
parent 3675302596
commit a9b7e22a13
4 changed files with 253 additions and 7 deletions

View File

@ -15,6 +15,7 @@ module.exports = {
}, },
moduleNameMapper: { moduleNameMapper: {
"^@/assets/.*\\.(jpg|jpeg|png|gif|webp|svg)$": path.resolve(__dirname, "mocks/file.js"), "^@/assets/.*\\.(jpg|jpeg|png|gif|webp|svg)$": path.resolve(__dirname, "mocks/file.js"),
"\\.(css)$": path.resolve(__dirname, "mocks/style.js"),
"\\.(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"), "^@/(.*)$": path.resolve(frontendRoot, "src/$1"),
"^vue$": path.resolve(frontendRoot, "node_modules/vue/dist/vue.cjs.js"), "^vue$": path.resolve(frontendRoot, "node_modules/vue/dist/vue.cjs.js"),
@ -24,13 +25,8 @@ module.exports = {
}, },
coverageProvider: "v8", coverageProvider: "v8",
collectCoverageFrom: [ collectCoverageFrom: [
"src/auth.js", "src/**/*.js",
"src/components/MetricRow.vue", "src/**/*.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"], coverageReporters: ["text", "lcov", "json-summary"],
}; };

View File

@ -0,0 +1 @@
module.exports = {};

View File

@ -0,0 +1,181 @@
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
import { flushPromises, mount } from "@vue/test-utils";
import { ReadableStream } from "node:stream/web";
import { TextDecoder, TextEncoder } from "node:util";
import AiView from "../../../frontend/src/views/AiView.vue";
function jsonResponse(body, status = 200, statusText = "OK") {
return new Response(JSON.stringify(body), {
status,
statusText,
headers: { "content-type": "application/json" },
});
}
function streamResponse(chunks) {
const encoder = new TextEncoder();
return new Response(new ReadableStream({
start(controller) {
for (const chunk of chunks) controller.enqueue(encoder.encode(chunk));
controller.close();
},
}), {
status: 200,
headers: { "content-type": "text/plain" },
});
}
function installFetch(handler) {
global.fetch = jest.fn(async (url, options = {}) => handler(String(url), options));
}
describe("AI chat view", () => {
beforeEach(() => {
localStorage.clear();
Object.defineProperty(globalThis, "crypto", {
configurable: true,
value: { randomUUID: () => "uuid-1" },
});
Object.defineProperty(globalThis, "TextDecoder", {
configurable: true,
value: TextDecoder,
});
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: { writeText: jest.fn(async () => {}) },
});
Object.defineProperty(performance, "now", {
configurable: true,
value: jest.fn()
.mockReturnValueOnce(100)
.mockReturnValueOnce(145)
.mockReturnValue(200),
});
installFetch((url) => {
if (url.includes("/api/ai/info")) {
return jsonResponse({
model: url.includes("atlas-smart") ? "smart-model" : "quick-model",
gpu: "titan-24",
node: "titan-24",
endpoint: "https://ai.example.dev/chat",
});
}
return jsonResponse({ reply: "typed assistant response", latency_ms: 42 });
});
});
afterEach(() => {
jest.restoreAllMocks();
localStorage.clear();
Reflect.deleteProperty(global, "fetch");
Reflect.deleteProperty(globalThis, "__ATLAS_IMPORT_META_ENV__");
});
it("loads profile metadata, switches profiles, and copies curl examples", async () => {
const wrapper = mount(AiView);
await flushPromises();
expect(wrapper.text()).toContain("quick-model");
expect(wrapper.text()).toContain("titan-24");
await wrapper.find(".endpoint-copy").trigger("click");
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(expect.stringContaining("curl -X POST https://ai.example.dev/chat"));
expect(wrapper.text()).toContain("copied");
await wrapper.findAll(".profile-tab").find((button) => button.text() === "Atlas Smart").trigger("click");
await flushPromises();
expect(wrapper.text()).toContain("smart-model");
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: { writeText: jest.fn(async () => { throw new Error("blocked"); }) },
});
await wrapper.find(".endpoint-copy").trigger("click");
expect(wrapper.text()).not.toContain("copied");
wrapper.unmount();
});
it("sends JSON chat requests and reveals typed responses", async () => {
const bodies = [];
installFetch((url, options) => {
if (url.includes("/api/ai/info")) return jsonResponse({ model: "quick-model" });
bodies.push(JSON.parse(options.body));
return jsonResponse({ reply: "typed assistant response", latency_ms: 42 });
});
const wrapper = mount(AiView);
await flushPromises();
await wrapper.find("textarea").setValue(" hello atlas ");
await wrapper.find("form").trigger("submit.prevent");
await new Promise((resolve) => setTimeout(resolve, 80));
await flushPromises();
expect(wrapper.text()).toContain("hello atlas");
expect(wrapper.text()).toContain("typed assistant response");
expect(wrapper.text()).toContain("42 ms");
expect(bodies[0]).toMatchObject({
message: "hello atlas",
profile: "atlas-quick",
conversation_id: expect.stringContaining("atlas-quick"),
});
expect(localStorage.getItem("atlas-ai-conversation:atlas-quick")).toContain("uuid-1");
await wrapper.find("textarea").setValue("second");
await wrapper.find("form").trigger("submit.prevent");
await new Promise((resolve) => setTimeout(resolve, 80));
expect(bodies[1].history.map((item) => item.role)).toContain("assistant");
wrapper.unmount();
});
it("handles streaming responses and keyboard submission", async () => {
const seen = [];
installFetch((url, options) => {
if (url.includes("/api/ai/info")) return jsonResponse({}, 204, "No Content");
seen.push(JSON.parse(options.body));
return streamResponse(["stream ", "reply"]);
});
const wrapper = mount(AiView);
await flushPromises();
const preventDefault = jest.fn();
await wrapper.find("textarea").setValue("stream please");
await wrapper.find("textarea").trigger("keydown", { key: "Enter", shiftKey: false, preventDefault });
await flushPromises();
expect(preventDefault).toHaveBeenCalled();
expect(wrapper.text()).toContain("stream reply");
expect(seen).toHaveLength(1);
await wrapper.find("textarea").setValue("line one");
await wrapper.find("textarea").trigger("keydown", { key: "Enter", shiftKey: true, preventDefault: jest.fn() });
await flushPromises();
expect(seen).toHaveLength(1);
wrapper.unmount();
});
it("shows request failures without losing the conversation", async () => {
installFetch((url) => {
if (url.includes("/api/ai/info")) throw new Error("info offline");
return jsonResponse({ error: "model offline" }, 503, "Service Unavailable");
});
const wrapper = mount(AiView);
await flushPromises();
await wrapper.find("textarea").setValue("break");
await wrapper.find("form").trigger("submit.prevent");
await flushPromises();
expect(wrapper.text()).toContain("model offline");
expect(wrapper.text()).toContain("(no response)");
await wrapper.find("textarea").setValue(" ");
await wrapper.find("form").trigger("submit.prevent");
await flushPromises();
expect(fetch).toHaveBeenCalledTimes(2);
wrapper.unmount();
});
});

View File

@ -0,0 +1,68 @@
import { afterEach, describe, expect, it, jest } from "@jest/globals";
describe("frontend entrypoint and router", () => {
afterEach(() => {
jest.resetModules();
jest.restoreAllMocks();
});
it("registers the router, mounts the app, and starts auth", async () => {
const use = jest.fn();
const mount = jest.fn();
const createApp = jest.fn(() => ({ use, mount }));
const initAuth = jest.fn();
const router = { name: "router" };
const app = { name: "App" };
jest.doMock("vue", () => ({ createApp }));
jest.doMock("../../../frontend/src/App.vue", () => ({ __esModule: true, default: app }));
jest.doMock("../../../frontend/src/router", () => ({ __esModule: true, default: router }));
jest.doMock("../../../frontend/src/auth", () => ({ initAuth }));
await import("../../../frontend/src/main.js");
expect(createApp).toHaveBeenCalledWith(app);
expect(use).toHaveBeenCalledWith(router);
expect(mount).toHaveBeenCalledWith("#app");
expect(initAuth).toHaveBeenCalledTimes(1);
});
it("declares the application routes and ai redirect", async () => {
jest.dontMock("../../../frontend/src/router");
for (const view of [
"HomeView",
"AboutView",
"AiView",
"AiPlanView",
"MoneroView",
"AppsView",
"AccountView",
"RequestAccessView",
"OnboardingView",
]) {
jest.doMock(`../../../frontend/src/views/${view}.vue`, () => ({ __esModule: true, default: { name: view } }));
}
const createWebHistory = jest.fn(() => ({ mode: "history" }));
const createRouter = jest.fn((config) => ({ ...config, ready: true }));
jest.doMock("vue-router", () => ({ createRouter, createWebHistory }), { virtual: true });
const router = (await import("../../../frontend/src/router.js")).default;
expect(createWebHistory).toHaveBeenCalledTimes(1);
expect(createRouter).toHaveBeenCalledWith(expect.objectContaining({ history: { mode: "history" } }));
expect(router.routes.map((route) => route.path)).toEqual([
"/",
"/about",
"/ai",
"/ai/chat",
"/ai/roadmap",
"/monero",
"/apps",
"/account",
"/request-access",
"/onboarding",
]);
expect(router.routes.find((route) => route.path === "/ai")).toMatchObject({ redirect: "/ai/chat" });
expect(router.routes.find((route) => route.name === "account").component).toBeDefined();
});
});