moadim 1.7.4

Loop engine for AI agents — routines over REST, MCP, and a built-in web UI
import { fireEvent, render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { describe, expect, it, vi } from "vitest";
import type { NotificationEntry } from "../lib/notificationLog";
import { NotificationCenter } from "./NotificationCenter";

function entry(overrides: Partial<NotificationEntry> = {}): NotificationEntry {
  return {
    id: "wb-1",
    routineId: "r1",
    routineTitle: "Nightly backup",
    message: "Run failed (exit 1)",
    atSecs: Math.floor(Date.now() / 1000) - 60,
    read: false,
    ...overrides,
  };
}

function renderCenter(entries: NotificationEntry[]) {
  const onMarkAllRead = vi.fn();
  const onClear = vi.fn();
  render(
    <MemoryRouter>
      <NotificationCenter entries={entries} onMarkAllRead={onMarkAllRead} onClear={onClear} />
    </MemoryRouter>,
  );
  return { onMarkAllRead, onClear };
}

describe("NotificationCenter", () => {
  it("shows no badge when there are no unread entries", () => {
    renderCenter([entry({ read: true })]);
    expect(screen.queryByText("1")).not.toBeInTheDocument();
  });

  it("shows an unread-count badge on the bell", () => {
    renderCenter([entry(), entry({ id: "wb-2", read: true })]);
    expect(screen.getByText("1")).toBeInTheDocument();
  });

  it("caps the displayed badge count at 99+", () => {
    const entries = Array.from({ length: 150 }, (_, i) => entry({ id: `wb-${i}` }));
    renderCenter(entries);
    expect(screen.getByText("99+")).toBeInTheDocument();
  });

  it("panel is closed until the bell is clicked", () => {
    renderCenter([entry()]);
    expect(screen.queryByRole("menu")).not.toBeInTheDocument();
    fireEvent.click(screen.getByRole("button", { name: /unread failure/ }));
    expect(screen.getByRole("menu")).toBeInTheDocument();
  });

  it("shows an empty state when there are no notifications", () => {
    renderCenter([]);
    fireEvent.click(screen.getByRole("button", { name: "Notifications" }));
    expect(screen.getByText("No notifications yet")).toBeInTheDocument();
  });

  it("lists entries with routine title and message", () => {
    renderCenter([entry()]);
    fireEvent.click(screen.getByRole("button", { name: /unread failure/ }));
    expect(screen.getByText("Nightly backup")).toBeInTheDocument();
    expect(screen.getByText("Run failed (exit 1)")).toBeInTheDocument();
  });

  it("calls onMarkAllRead when MARK READ is clicked", () => {
    const { onMarkAllRead } = renderCenter([entry()]);
    fireEvent.click(screen.getByRole("button", { name: /unread failure/ }));
    fireEvent.click(screen.getByText("MARK READ"));
    expect(onMarkAllRead).toHaveBeenCalledTimes(1);
  });

  it("does not show MARK READ when nothing is unread", () => {
    renderCenter([entry({ read: true })]);
    fireEvent.click(screen.getByRole("button", { name: "Notifications" }));
    expect(screen.queryByText("MARK READ")).not.toBeInTheDocument();
  });

  it("calls onClear when CLEAR is clicked", () => {
    const { onClear } = renderCenter([entry()]);
    fireEvent.click(screen.getByRole("button", { name: /unread failure/ }));
    fireEvent.click(screen.getByText("CLEAR"));
    expect(onClear).toHaveBeenCalledTimes(1);
  });

  it("closes the panel on Escape", () => {
    renderCenter([entry()]);
    fireEvent.click(screen.getByRole("button", { name: /unread failure/ }));
    expect(screen.getByRole("menu")).toBeInTheDocument();
    fireEvent.keyDown(document, { key: "Escape" });
    expect(screen.queryByRole("menu")).not.toBeInTheDocument();
  });

  it("closes the panel on outside click", () => {
    renderCenter([entry()]);
    fireEvent.click(screen.getByRole("button", { name: /unread failure/ }));
    expect(screen.getByRole("menu")).toBeInTheDocument();
    fireEvent.mouseDown(document.body);
    expect(screen.queryByRole("menu")).not.toBeInTheDocument();
  });
});