Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// @vitest-environment jsdom
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";

import { BlockHeader } from "./block-header";

afterEach(() => {
cleanup();
});

describe("BlockHeader", () => {
it("renders nothing when title, description, and count are all absent", () => {
const { container } = render(<BlockHeader />);
expect(container.firstChild).toBeNull();
});

it("renders the title alone as an h3, with no description markup", () => {
const { container } = render(<BlockHeader title="Deploy status" />);
expect(screen.getByText("Deploy status").tagName).toBe("H3");
// The description markdown is gated on `description` being present —
// the title row should be the header's only child when it is absent.
expect(container.querySelector(".mb-1\\.5")?.children.length).toBe(1);
});

it("renders a count of 0 on its own, proving the check is `!== undefined` not truthiness", () => {
const { container } = render(<BlockHeader count={0} />);
expect(screen.getByText("(0)")).toBeTruthy();
expect(container.querySelector("h3")).toBeNull();
});

it("renders the count parenthesized next to the title", () => {
render(<BlockHeader title="Items" count={5} />);
expect(screen.getByText("(5)")).toBeTruthy();
});

it("renders description as markdown when title and count are both absent", () => {
const { container } = render(<BlockHeader description="Extra context." />);
expect(screen.getByText("Extra context.")).toBeTruthy();
expect(container.querySelector("h3")).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// @vitest-environment jsdom
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";

import type { StatusBlock } from "@/components/app/agent-surfaces/types";
import { formatSurfaceTime } from "@/components/app/agent-surfaces/format";
import { TONE_CLASSES } from "@/components/app/agent-surfaces/tone";
import { StatusBlockView } from "./status-block";

afterEach(() => {
vi.useRealTimers();
cleanup();
});

function block(overrides: Partial<StatusBlock> = {}): StatusBlock {
return {
id: "s1",
type: "status",
status: "Running",
...overrides,
} as StatusBlock;
}

describe("StatusBlockView", () => {
it("defaults to neutral tone when none is authored", () => {
const { container } = render(<StatusBlockView block={block()} />);
const dot = container.querySelector('[aria-hidden="true"]');
expect(dot?.className).toContain(TONE_CLASSES.neutral.dot);
expect(screen.getByText("Running").className).toContain(
TONE_CLASSES.neutral.text
);
});

it("applies the authored tone's dot and text classes", () => {
const { container } = render(
<StatusBlockView block={block({ tone: "danger" })} />
);
const dot = container.querySelector('[aria-hidden="true"]');
expect(dot?.className).toContain(TONE_CLASSES.danger.dot);
expect(screen.getByText("Running").className).toContain(
TONE_CLASSES.danger.text
);
});

it("renders no <time> element when the block has no timestamp", () => {
const { container } = render(<StatusBlockView block={block()} />);
expect(container.querySelector("time")).toBeNull();
});

it("renders the relative time with the absolute on the title attribute", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-09-02T12:00:00Z"));
const iso = "2026-09-02T11:15:00Z";
const expected = formatSurfaceTime(iso);

render(<StatusBlockView block={block({ timestamp: iso })} />);

const time = screen.getByText(`· ${expected.text}`);
expect(time.tagName).toBe("TIME");
expect(time.getAttribute("dateTime")).toBe(iso);
expect(time.getAttribute("title")).toBe(expected.absolute);
});

it("renders the detail markdown only when provided", () => {
const { container, rerender } = render(
<StatusBlockView block={block({ detail: "Retrying in 30s." })} />
);
expect(screen.getByText("Retrying in 30s.")).toBeTruthy();

rerender(<StatusBlockView block={block({})} />);
expect(screen.queryByText("Retrying in 30s.")).toBeNull();
// The detail Markdown wrapper carries "mt-0.5" — confirm it is gone
// entirely, not just emptied of text.
expect(container.querySelector(".mt-0\\.5")).toBeNull();
});

it("passes title/description through to the shared BlockHeader", () => {
render(
<StatusBlockView
block={block({ title: "Deploy", description: "prod-east-1" })}
/>
);
expect(screen.getByText("Deploy")).toBeTruthy();
expect(screen.getByText("prod-east-1")).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// @vitest-environment jsdom
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";

import type { TextBlock } from "@/components/app/agent-surfaces/types";
import { TONE_CLASSES } from "@/components/app/agent-surfaces/tone";
import { TextBlockView } from "./text-block";

afterEach(() => {
cleanup();
});

function block(overrides: Partial<TextBlock> = {}): TextBlock {
return {
id: "t1",
type: "text",
text: "Ready to ship.",
...overrides,
} as TextBlock;
}

describe("TextBlockView", () => {
it("renders plain prose with no callout wrapper when tone is absent", () => {
const { container } = render(<TextBlockView block={block()} />);
expect(screen.getByText("Ready to ship.")).toBeTruthy();
expect(container.querySelector("[data-tone]")).toBeNull();
});

it("renders plain prose when tone is explicitly neutral", () => {
const { container } = render(
<TextBlockView block={block({ tone: "neutral" })} />
);
expect(container.querySelector("[data-tone]")).toBeNull();
});

it("renders a toned callout with the tone's border and background classes", () => {
const { container } = render(
<TextBlockView block={block({ tone: "warning" })} />
);
const wrapper = container.querySelector('[data-tone="warning"]');
expect(wrapper).toBeTruthy();
const callout = wrapper?.firstElementChild;
const classes = callout?.className.split(" ") ?? [];
for (const cls of TONE_CLASSES.warning.callout.split(" ")) {
expect(classes).toContain(cls);
}
});

it("renders the toned title in the tone's text color only when authored", () => {
const { container, rerender } = render(
<TextBlockView block={block({ tone: "danger", title: "Blocked" })} />
);
const heading = screen.getByText("Blocked");
expect(heading.tagName).toBe("H3");
expect(heading.className).toContain(TONE_CLASSES.danger.text);

rerender(<TextBlockView block={block({ tone: "danger" })} />);
expect(screen.queryByText("Blocked")).toBeNull();
// Confirm the <h3> itself is gone, not just left empty of text.
expect(container.querySelector("h3")).toBeNull();
});

it("renders the toned description only when authored", () => {
const { container, rerender } = render(
<TextBlockView
block={block({ tone: "info", description: "Applies to prod only." })}
/>
);
expect(screen.getByText("Applies to prod only.")).toBeTruthy();

rerender(<TextBlockView block={block({ tone: "info" })} />);
expect(screen.queryByText("Applies to prod only.")).toBeNull();
// The description Markdown wrapper carries "mb-1" — confirm it is gone
// entirely, not just emptied of text.
expect(container.querySelector(".mb-1")).toBeNull();
});

it("always renders the body text inside the toned callout", () => {
render(
<TextBlockView block={block({ tone: "success", text: "All good." })} />
);
expect(screen.getByText("All good.")).toBeTruthy();
});
});
Loading