/** * Logs page — verifies the telemetry-driven log tail: * - Only `kind:"log"` records surface (non-log kinds are filtered out). * - Level filter narrows the list correctly. * - Message text filter narrows the list correctly. * - Fields panel is hidden by default and reveals on toggle. */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { mount, flushPromises } from "@vue/test-utils"; import { createRouter, createMemoryHistory } from "vue-router"; import Logs from "../Logs.vue"; const LOG_RECORDS = [ { kind: "log", level: "debug", message: "handler resolved", ts: 1000, fields: { handler: "placeOrder" }, }, { kind: "log", level: "info", message: "order placed", ts: 2000 }, { kind: "log", level: "warn", message: "retry scheduled", ts: 3000, fields: { attempt: 2 } }, { kind: "log", level: "error", message: "payment failed", ts: 4000, fields: { code: "DECLINED" }, }, // Non-log record — must be invisible on this page. { kind: "action.dispatched", name: "orders.place", ts: 5000 }, ]; let streamRecords: unknown[] = []; class FakeEventSource { onopen: (() => void) | null = null; onerror: (() => void) | null = null; onmessage: ((m: { data: string }) => void) | null = null; listeners: Record void>> = {}; constructor(public readonly url: string) { queueMicrotask(() => { this.onopen?.(); for (const rec of streamRecords) this.onmessage?.({ data: JSON.stringify(rec) }); }); } addEventListener(type: string, fn: (ev: { data: string }) => void): void { (this.listeners[type] ??= []).push(fn); } removeEventListener(): void {} close(): void {} } beforeEach(() => { streamRecords = LOG_RECORDS; (globalThis as unknown as { EventSource: typeof FakeEventSource }).EventSource = FakeEventSource; globalThis.fetch = vi.fn((url: string | URL) => { const u = String(url); if (u.includes("/__nwire/telemetry/runs")) { return Promise.resolve(new Response(JSON.stringify({ runs: [] }), { status: 200 })); } return Promise.resolve(new Response("", { status: 404 })); }) as typeof fetch; }); function makeRouter() { return createRouter({ history: createMemoryHistory(), routes: [ { path: "/logs", name: "logs", component: Logs }, { path: "/", name: "home", component: { template: "
" } }, ], }); } describe("Logs page", () => { it("renders only kind:log records — non-log telemetry is hidden", async () => { const router = makeRouter(); router.push("/logs"); await router.isReady(); const wrapper = mount(Logs, { global: { plugins: [router] } }); await flushPromises(); // 4 log records, not 5 (the action.dispatched must be excluded). const rows = wrapper.findAll('[data-testid^="log-row-"]'); expect(rows).toHaveLength(4); }); it("displays level badges with the correct label", async () => { const router = makeRouter(); router.push("/logs"); await router.isReady(); const wrapper = mount(Logs, { global: { plugins: [router] } }); await flushPromises(); const badges = wrapper.findAll('[data-testid^="level-badge-"]'); const levels = badges.map((b) => b.text().trim().toLowerCase()); expect(levels).toContain("debug"); expect(levels).toContain("info"); expect(levels).toContain("warn"); expect(levels).toContain("error"); }); it("level filter: selecting 'error' shows only error rows", async () => { const router = makeRouter(); router.push("/logs"); await router.isReady(); const wrapper = mount(Logs, { global: { plugins: [router] } }); await flushPromises(); const select = wrapper.get('[data-testid="level-filter"]'); await select.setValue("error"); await flushPromises(); const rows = wrapper.findAll('[data-testid^="log-row-"]'); expect(rows).toHaveLength(1); expect(wrapper.get('[data-testid="log-message-0"]').text()).toBe("payment failed"); }); it("message filter: text search narrows to matching records", async () => { const router = makeRouter(); router.push("/logs"); await router.isReady(); const wrapper = mount(Logs, { global: { plugins: [router] } }); await flushPromises(); const input = wrapper.get('[data-testid="filter-input"]'); await input.setValue("order"); await flushPromises(); // Matches "handler resolved" (no), "order placed" (yes), "retry scheduled" (no), "payment failed" (no). const rows = wrapper.findAll('[data-testid^="log-row-"]'); expect(rows).toHaveLength(1); expect(wrapper.get('[data-testid="log-message-0"]').text()).toBe("order placed"); }); it("fields panel is hidden by default and expands on toggle", async () => { const router = makeRouter(); router.push("/logs"); await router.isReady(); const wrapper = mount(Logs, { global: { plugins: [router] } }); await flushPromises(); // Row 0 (debug) has fields — panel hidden initially. expect(wrapper.find('[data-testid="fields-panel-0"]').exists()).toBe(false); await wrapper.get('[data-testid="fields-toggle-0"]').trigger("click"); expect(wrapper.find('[data-testid="fields-panel-0"]').exists()).toBe(true); // Toggle again to collapse. await wrapper.get('[data-testid="fields-toggle-0"]').trigger("click"); expect(wrapper.find('[data-testid="fields-panel-0"]').exists()).toBe(false); }); it("rows without fields have no fields toggle", async () => { const router = makeRouter(); router.push("/logs"); await router.isReady(); const wrapper = mount(Logs, { global: { plugins: [router] } }); await flushPromises(); // Row 1 (info "order placed") has no fields. expect(wrapper.find('[data-testid="fields-toggle-1"]').exists()).toBe(false); }); it("shows the empty state when no log records have arrived", async () => { streamRecords = []; const router = makeRouter(); router.push("/logs"); await router.isReady(); const wrapper = mount(Logs, { global: { plugins: [router] } }); await flushPromises(); expect(wrapper.find('[data-testid="empty-state"]').exists()).toBe(true); }); });