Meet Frontfriend.Your brand. Your rules. Your agents, connected.Explore what’s new

Test Results

A test run grouped by suite, with passed, failed, and skipped tests and failure details.

Interactive preview

The shared preview uses React Base. The source below uses Radix UI. Each example runs with local data.

Installation

Complete project setup with react/radix, then install these components:

pnpm exec frontfriend download trace code-block icon button message bubble -f react --primitive radix

Count results from the tests, not from the tool status. Open failed suites and failed tests by default. A skipped test is distinct from a test that is waiting to run. The example uses fixed test results and does not execute tests.

Usage

Complete example

import { Message, MessageContent } from "@/components/ui/message";
import { Bubble, BubbleContent } from "@/components/ui/bubble";
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleTrigger,
  TraceCollapsibleContent,
  TraceLine,
  TraceIcon,
  TraceTitle,
} from "@/components/ui/trace";
import { Icon } from "@/components/ui/icon";
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block";
import { Button } from "@/components/ui/button";
import * as React from "react";
type Phase = "complete" | "running" | "error" | "pending";
function useRun(initial: Phase) {
  const [phase, setPhase] = React.useState(initial);
  const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(
    undefined,
  );
  React.useEffect(() => {
    clearTimeout(timer.current);
    setPhase(initial);
  }, [initial]);
  React.useEffect(() => () => clearTimeout(timer.current), []);
  function replay() {
    clearTimeout(timer.current);
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      setPhase("complete");
      return;
    }
    setPhase("running");
    timer.current = setTimeout(() => setPhase("complete"), 3600);
  }
  return { phase, replay };
}
const frame: React.CSSProperties = {
  width: "100%",
  maxWidth: 384,
  minWidth: 0,
  minHeight: 288,
  marginInline: "auto",
  display: "flex",
  flexDirection: "column",
  gap: 16,
  paddingBlock: 24,
  fontSize: "var(--text-sm)",
  lineHeight: 1.5,
};
function TracePanel({
  trigger,
  children,
  open: controlledOpen,
  defaultOpen = false,
  onOpenChange,
  ...props
}: React.ComponentProps<typeof TraceCollapsible> & {
  trigger: React.ReactNode;
}) {
  const contentRef = React.useRef<HTMLDivElement>(null);
  const [height, setHeight] = React.useState<number>();
  const [localOpen, setLocalOpen] = React.useState(defaultOpen);
  const expanded = controlledOpen ?? localOpen;
  const [hasOpened, setHasOpened] = React.useState(expanded);
  React.useEffect(() => {
    if (expanded) setHasOpened(true);
  }, [expanded]);
  React.useLayoutEffect(() => {
    const content = contentRef.current;
    const inner = content?.firstElementChild;
    if (!expanded || !content || !inner) return;
    const measure = () => setHeight(content.scrollHeight);
    measure();
    const observer = new ResizeObserver(measure);
    observer.observe(inner);
    return () => observer.disconnect();
  }, [expanded]);
  return (
    <TraceCollapsible
      {...props}
      open={expanded}
      onOpenChange={(value) => {
        if (!value && contentRef.current) {
          setHeight(contentRef.current.getBoundingClientRect().height);
        }
        setLocalOpen(value);
        onOpenChange?.(value);
      }}
    >
      <TraceCollapsibleTrigger>{trigger}</TraceCollapsibleTrigger>
      <TraceCollapsibleContent
        ref={contentRef}
        style={
          height === undefined
            ? undefined
            : ({
                "--radix-collapsible-content-height": `${height}px`,
              } as React.CSSProperties)
        }
        forceMount={hasOpened ? true : undefined}
        inert={!expanded}
      >
        {children}
      </TraceCollapsibleContent>
    </TraceCollapsible>
  );
}
type TestResult = {
  name: string;
  status: "passed" | "failed" | "skipped";
  error?: string;
};
const suites: {
  name: string;
  tests: TestResult[];
}[] = [
  {
    name: "cart.test.ts",
    tests: [
      { name: "adds an item", status: "passed" },
      { name: "removes an item", status: "passed" },
      { name: "applies a discount code", status: "passed" },
    ],
  },
  {
    name: "payment.test.ts",
    tests: [
      { name: "charges the saved card", status: "passed" },
      {
        name: "retries a declined card",
        status: "failed",
        error:
          "Expected 2 charge attempts, received 1\n\n  at src/payment.test.ts:58:22",
      },
      { name: "refunds a payment", status: "skipped" },
    ],
  },
];
function counts(tests: TestResult[]) {
  const failed = tests.filter((test) => test.status === "failed").length;
  const skipped = tests.filter((test) => test.status === "skipped").length;
  return `${tests.length} tests${failed ? `, ${failed} failed` : ""}${skipped ? `, ${skipped} skipped` : ""}`;
}
export default function AiTestResultsExample() {
  const { phase, replay } = useRun("complete");
  const [open, setOpen] = React.useState<Record<string, boolean>>({});
  function expand(id: string, value: boolean) {
    setOpen((current) => ({ ...current, [id]: value }));
  }
  return (
    <div style={frame}>
      <Message align="end">
        <MessageContent>
          <Bubble align="end" variant="muted">
            <BubbleContent>Run the checkout tests.</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Message>
        <MessageContent style={{ gap: 16, minWidth: 0 }}>
          <Trace>
            {phase === "complete" ? (
              <TracePanel
                open={
                  open.run ??
                  suites.some((suite) =>
                    suite.tests.some((test) => test.status === "failed"),
                  )
                }
                onOpenChange={(value) => expand("run", value)}
                trigger={
                  <TraceLine
                    status={
                      suites.some((suite) =>
                        suite.tests.some((test) => test.status === "failed"),
                      )
                        ? "error"
                        : undefined
                    }
                  >
                    <TraceIcon>
                      <Icon name="FlaskConical" />
                    </TraceIcon>
                    <TraceTitle>
                      Ran {counts(suites.flatMap((suite) => suite.tests))}
                    </TraceTitle>
                  </TraceLine>
                }
              >
                <Trace>
                  {suites.map((suite) => (
                    <TracePanel
                      key={suite.name}
                      open={
                        open[suite.name] ??
                        suite.tests.some((test) => test.status === "failed")
                      }
                      onOpenChange={(value) => expand(suite.name, value)}
                      trigger={
                        <TraceLine
                          status={
                            suite.tests.some((test) => test.status === "failed")
                              ? "error"
                              : undefined
                          }
                        >
                          <TraceIcon>
                            <Icon name="FileCode" />
                          </TraceIcon>
                          <TraceTitle>
                            {suite.name}, {counts(suite.tests)}
                          </TraceTitle>
                        </TraceLine>
                      }
                    >
                      <Trace>
                        {suite.tests.map((test) =>
                          test.status === "failed" ? (
                            <TracePanel
                              key={test.name}
                              open={open[test.name] ?? test.status === "failed"}
                              onOpenChange={(value) => expand(test.name, value)}
                              trigger={
                                <TraceLine status="error">
                                  <TraceIcon>
                                    <Icon name="X" />
                                  </TraceIcon>
                                  <TraceTitle>{test.name}</TraceTitle>
                                </TraceLine>
                              }
                            >
                              <CodeBlock size="sm">
                                <CodeBlockContent wrap>
                                  {test.error}
                                </CodeBlockContent>
                              </CodeBlock>
                            </TracePanel>
                          ) : (
                            <TraceLine key={test.name}>
                              <TraceIcon>
                                <Icon
                                  name={
                                    test.status === "skipped"
                                      ? "CircleDashed"
                                      : "Check"
                                  }
                                />
                              </TraceIcon>
                              <TraceTitle>
                                {test.status === "skipped" ? "Skipped " : ""}
                                {test.name}
                              </TraceTitle>
                            </TraceLine>
                          ),
                        )}
                      </Trace>
                    </TracePanel>
                  ))}
                </Trace>
              </TracePanel>
            ) : (
              <TraceLine status={phase}>
                <TraceIcon>
                  <Icon name="FlaskConical" />
                </TraceIcon>
                <TraceTitle>
                  {`${
                    phase === "running"
                      ? "Running"
                      : phase === "pending"
                        ? "Waiting to run"
                        : "Failed to run"
                  } pnpm test checkout`}
                </TraceTitle>
              </TraceLine>
            )}
          </Trace>
          {phase === "complete" && (
            <p>
              One test failed. The payment client does not retry a declined
              card, so the second charge attempt never happens.
            </p>
          )}
        </MessageContent>
      </Message>
      <div>
        <Button
          size="sm"
          variant="outline"
          onClick={replay}
          disabled={phase === "running"}
        >
          Replay
        </Button>
      </div>
    </div>
  );
}

AI SDK

The runTests tool returns suites with stable suite and test IDs, names, passed/failed/skipped status, and optional error text. The adapter derives counts from part.output.suites and opens failed suites. Tool execution errors use part.errorText; failed assertions use each test error.

The basic example runs without a provider. For the AI SDK adapter example only, install the SDK types:

pnpm add ai

Pass a message from your chat client to the adapter. The supplied message does not call a provider.

import type { UIMessage } from "ai";
import {
  Trace,
  TraceLine,
  TraceIcon,
  TraceTitle,
  TraceCollapsible,
  TraceCollapsibleTrigger,
  TraceCollapsibleContent,
} from "@/components/ui/trace";
import { Icon } from "@/components/ui/icon";
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block";
import * as React from "react";
const frame: React.CSSProperties = {
  width: "100%",
  maxWidth: 384,
  minWidth: 0,
  minHeight: 288,
  marginInline: "auto",
  display: "flex",
  flexDirection: "column",
  gap: 16,
  paddingBlock: 24,
  fontSize: "var(--text-sm)",
  lineHeight: 1.5,
};
type SdkTest = {
  id: string;
  name: string;
  status: "passed" | "failed" | "skipped";
  error?: string;
};
type SdkSuite = {
  id: string;
  name: string;
  tests: SdkTest[];
};
type SdkTestMessage = UIMessage<
  unknown,
  {},
  {
    runTests: {
      input: {
        command: string;
      };
      output: {
        suites: SdkSuite[];
      };
    };
  }
>;
function TracePanel({
  trigger,
  children,
  open: controlledOpen,
  defaultOpen = false,
  onOpenChange,
  ...props
}: React.ComponentProps<typeof TraceCollapsible> & {
  trigger: React.ReactNode;
}) {
  const contentRef = React.useRef<HTMLDivElement>(null);
  const [height, setHeight] = React.useState<number>();
  const [localOpen, setLocalOpen] = React.useState(defaultOpen);
  const expanded = controlledOpen ?? localOpen;
  const [hasOpened, setHasOpened] = React.useState(expanded);
  React.useEffect(() => {
    if (expanded) setHasOpened(true);
  }, [expanded]);
  React.useLayoutEffect(() => {
    const content = contentRef.current;
    const inner = content?.firstElementChild;
    if (!expanded || !content || !inner) return;
    const measure = () => setHeight(content.scrollHeight);
    measure();
    const observer = new ResizeObserver(measure);
    observer.observe(inner);
    return () => observer.disconnect();
  }, [expanded]);
  return (
    <TraceCollapsible
      {...props}
      open={expanded}
      onOpenChange={(value) => {
        if (!value && contentRef.current) {
          setHeight(contentRef.current.getBoundingClientRect().height);
        }
        setLocalOpen(value);
        onOpenChange?.(value);
      }}
    >
      <TraceCollapsibleTrigger>{trigger}</TraceCollapsibleTrigger>
      <TraceCollapsibleContent
        ref={contentRef}
        style={
          height === undefined
            ? undefined
            : ({
                "--radix-collapsible-content-height": `${height}px`,
              } as React.CSSProperties)
        }
        forceMount={hasOpened ? true : undefined}
        inert={!expanded}
      >
        {children}
      </TraceCollapsibleContent>
    </TraceCollapsible>
  );
}
function SdkPanel({
  title,
  status,
  children,
  initialOpen = false,
}: {
  title: string;
  status?: "running" | "pending" | "error";
  children: React.ReactNode;
  initialOpen?: boolean;
}) {
  const [open, setOpen] = React.useState<boolean>();
  const expanded = open ?? initialOpen;
  return (
    <TracePanel
      open={expanded}
      onOpenChange={setOpen}
      trigger={
        <TraceLine status={status}>
          <TraceIcon>
            <Icon name={status === "error" ? "CircleAlert" : "Terminal"} />
          </TraceIcon>
          <TraceTitle>{title}</TraceTitle>
        </TraceLine>
      }
    >
      {children}
    </TracePanel>
  );
}
function TestResultsSdkAdapter({
  message,
  streaming,
}: {
  message: SdkTestMessage;
  streaming: boolean;
}) {
  return (
    <Trace>
      {message.parts.map((part) => {
        if (part.type !== "tool-runTests") return null;
        if (part.state !== "output-available" || part.preliminary) {
          const pending = part.state === "approval-requested";
          const denied =
            part.state === "output-denied" ||
            (part.state === "approval-responded" && !part.approval.approved);
          const failed =
            part.state === "output-error" ||
            (!pending && !denied && !streaming);
          const title = `${denied ? "Denied" : pending ? "Waiting to run" : failed ? "Failed to run" : "Running"} ${part.input?.command ?? "tests"}`;
          return (
            <div key={part.toolCallId}>
              <TraceLine
                status={
                  pending
                    ? "pending"
                    : failed
                      ? "error"
                      : denied
                        ? undefined
                        : "running"
                }
              >
                <TraceIcon>
                  <Icon name="FlaskConical" />
                </TraceIcon>
                <TraceTitle>{title}</TraceTitle>
              </TraceLine>
              {part.state === "output-error" && (
                <CodeBlock size="sm" variant="destructive">
                  <CodeBlockContent wrap>{part.errorText}</CodeBlockContent>
                </CodeBlock>
              )}
            </div>
          );
        }
        const tests = part.output.suites.flatMap((suite) => suite.tests),
          failed = tests.filter((test) => test.status === "failed").length,
          skipped = tests.filter((test) => test.status === "skipped").length;
        return (
          <SdkPanel
            key={part.toolCallId}
            title={`Ran ${tests.length} tests${failed ? `, ${failed} failed` : ""}${skipped ? `, ${skipped} skipped` : ""}`}
            status={failed ? "error" : undefined}
            initialOpen={failed > 0}
          >
            <Trace>
              {part.output.suites.map((suite) => {
                const failed = suite.tests.filter(
                  (test) => test.status === "failed",
                ).length;
                return (
                  <SdkPanel
                    key={suite.id}
                    title={`${suite.name}, ${suite.tests.length} tests${failed ? `, ${failed} failed` : ""}`}
                    status={failed ? "error" : undefined}
                    initialOpen={failed > 0}
                  >
                    <Trace>
                      {suite.tests.map((test) =>
                        test.error ? (
                          <SdkPanel
                            key={test.id}
                            title={test.name}
                            status={
                              test.status === "failed" ? "error" : undefined
                            }
                            initialOpen
                          >
                            <CodeBlock size="sm" variant="destructive">
                              <CodeBlockContent wrap>
                                {test.error}
                              </CodeBlockContent>
                            </CodeBlock>
                          </SdkPanel>
                        ) : (
                          <TraceLine
                            key={test.id}
                            status={
                              test.status === "failed" ? "error" : undefined
                            }
                          >
                            <TraceIcon>
                              <Icon
                                name={
                                  test.status === "skipped"
                                    ? "CircleDashed"
                                    : test.status === "failed"
                                      ? "X"
                                      : "Check"
                                }
                              />
                            </TraceIcon>
                            <TraceTitle>
                              {test.status === "skipped"
                                ? `Skipped ${test.name}`
                                : test.name}
                            </TraceTitle>
                          </TraceLine>
                        ),
                      )}
                    </Trace>
                  </SdkPanel>
                );
              })}
            </Trace>
          </SdkPanel>
        );
      })}
    </Trace>
  );
}
const sdkMessage: SdkTestMessage = {
  id: "tests",
  role: "assistant",
  parts: [
    {
      type: "tool-runTests",
      toolCallId: "checkout-tests",
      state: "output-available",
      input: { command: "pnpm test" },
      output: {
        suites: [
          {
            id: "cart",
            name: "cart.test.ts",
            tests: [
              {
                id: "total",
                name: "calculates the order total",
                status: "passed",
              },
            ],
          },
          {
            id: "payment",
            name: "payment.test.ts",
            tests: [
              {
                id: "retry",
                name: "retries a declined card",
                status: "failed",
                error:
                  "Expected 2 charge attempts, received 1\n  at src/payment.test.ts:58:22",
              },
              {
                id: "refund",
                name: "refunds a payment",
                status: "skipped",
              },
            ],
          },
        ],
      },
    },
  ],
};
export default function AiTestResultsExample() {
  return (
    <div style={frame}>
      <TestResultsSdkAdapter message={sdkMessage} streaming={false} />
    </div>
  );
}

Suites

import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleTrigger,
  TraceCollapsibleContent,
  TraceLine,
  TraceIcon,
  TraceTitle,
} from "@/components/ui/trace";
import { Icon } from "@/components/ui/icon";
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block";
import * as React from "react";
const frame: React.CSSProperties = {
  width: "100%",
  maxWidth: 384,
  minWidth: 0,
  minHeight: 288,
  marginInline: "auto",
  display: "flex",
  flexDirection: "column",
  gap: 16,
  paddingBlock: 24,
  fontSize: "var(--text-sm)",
  lineHeight: 1.5,
};
type Test = {
  name: string;
  status: "passed" | "failed" | "skipped";
  error?: string;
};
function namedTestResultsSuitesCountTests(tests: Test[]) {
  return {
    total: tests.length,
    failed: tests.filter((test) => test.status === "failed").length,
    skipped: tests.filter((test) => test.status === "skipped").length,
  };
}
const namedTestResultsSuitesSuites: {
  name: string;
  tests: Test[];
}[] = [
  {
    name: "auth.test.ts",
    tests: [
      {
        name: "logs in with valid credentials",
        status: "passed",
      },
      {
        name: "rejects an invalid password",
        status: "passed",
      },
      {
        name: "refreshes an expired token",
        status: "passed",
      },
    ],
  },
  {
    name: "user-api.test.ts",
    tests: [
      {
        name: "creates a user",
        status: "passed",
      },
      {
        name: "updates a user profile",
        status: "failed",
        error: `Expected status 200, received 500

  at Object.<anonymous> (src/user-api.test.ts:45:12)`,
      },
      {
        name: "deletes a user",
        status: "skipped",
      },
    ],
  },
  {
    name: "db.test.ts",
    tests: [
      {
        name: "connects to the database",
        status: "passed",
      },
      {
        name: "times out a slow query",
        status: "failed",
        error: `Exceeded timeout of 5000 ms for a test.

  at Object.<anonymous> (src/db.test.ts:22:3)`,
      },
    ],
  },
];
function TracePanel({
  trigger,
  children,
  open: controlledOpen,
  defaultOpen = false,
  onOpenChange,
  ...props
}: React.ComponentProps<typeof TraceCollapsible> & {
  trigger: React.ReactNode;
}) {
  const contentRef = React.useRef<HTMLDivElement>(null);
  const [height, setHeight] = React.useState<number>();
  const [localOpen, setLocalOpen] = React.useState(defaultOpen);
  const expanded = controlledOpen ?? localOpen;
  const [hasOpened, setHasOpened] = React.useState(expanded);
  React.useEffect(() => {
    if (expanded) setHasOpened(true);
  }, [expanded]);
  React.useLayoutEffect(() => {
    const content = contentRef.current;
    const inner = content?.firstElementChild;
    if (!expanded || !content || !inner) return;
    const measure = () => setHeight(content.scrollHeight);
    measure();
    const observer = new ResizeObserver(measure);
    observer.observe(inner);
    return () => observer.disconnect();
  }, [expanded]);
  return (
    <TraceCollapsible
      {...props}
      open={expanded}
      onOpenChange={(value) => {
        if (!value && contentRef.current) {
          setHeight(contentRef.current.getBoundingClientRect().height);
        }
        setLocalOpen(value);
        onOpenChange?.(value);
      }}
    >
      <TraceCollapsibleTrigger>{trigger}</TraceCollapsibleTrigger>
      <TraceCollapsibleContent
        ref={contentRef}
        style={
          height === undefined
            ? undefined
            : ({
                "--radix-collapsible-content-height": `${height}px`,
              } as React.CSSProperties)
        }
        forceMount={hasOpened ? true : undefined}
        inert={!expanded}
      >
        {children}
      </TraceCollapsibleContent>
    </TraceCollapsible>
  );
}
function namedTestResultsSuitesFormatCounts({
  total,
  failed,
  skipped,
}: ReturnType<typeof namedTestResultsSuitesCountTests>) {
  return [
    `${total} tests`,
    failed > 0 && `${failed} failed`,
    skipped > 0 && `${skipped} skipped`,
  ]
    .filter(Boolean)
    .join(", ");
}
type TestResult = {
  name: string;
  status: "passed" | "failed" | "skipped";
  error?: string;
};
function counts(tests: TestResult[]) {
  const failed = tests.filter((test) => test.status === "failed").length;
  const skipped = tests.filter((test) => test.status === "skipped").length;
  return `${tests.length} tests${failed ? `, ${failed} failed` : ""}${skipped ? `, ${skipped} skipped` : ""}`;
}
function NamedTestResultsSuitesTestRow({ test }: { test: Test }) {
  if (test.status === "failed") {
    return (
      <TracePanel
        trigger={
          <TraceLine status="error">
            <TraceIcon>
              <Icon name="X" />
            </TraceIcon>
            <TraceTitle>{test.name}</TraceTitle>
          </TraceLine>
        }
      >
        <CodeBlock size="sm">
          <CodeBlockContent
            wrap
            style={{
              maxHeight: 256,
            }}
          >
            {test.error}
          </CodeBlockContent>
        </CodeBlock>
      </TracePanel>
    );
  }
  return (
    <TraceLine>
      <TraceIcon>
        {test.status === "skipped" ? (
          <Icon name="CircleDashed" />
        ) : (
          <Icon name="Check" />
        )}
      </TraceIcon>
      <TraceTitle>
        {test.status === "skipped" ? `Skipped ${test.name}` : test.name}
      </TraceTitle>
    </TraceLine>
  );
}
function TestResultsSuitesExample() {
  const summary = namedTestResultsSuitesCountTests(
    namedTestResultsSuitesSuites.flatMap((suite) => suite.tests),
  );
  return (
    <Trace>
      <TracePanel
        defaultOpen
        trigger={
          <TraceLine status={summary.failed > 0 ? "error" : undefined}>
            <TraceIcon>
              <Icon name="FlaskConical" />
            </TraceIcon>
            <TraceTitle>
              Ran {namedTestResultsSuitesFormatCounts(summary)}
            </TraceTitle>
          </TraceLine>
        }
      >
        <Trace>
          {namedTestResultsSuitesSuites.map((suite) => {
            const counts = namedTestResultsSuitesCountTests(suite.tests);
            return (
              <TracePanel
                key={suite.name}
                defaultOpen={counts.failed > 0}
                trigger={
                  <TraceLine status={counts.failed > 0 ? "error" : undefined}>
                    <TraceIcon>
                      <Icon name="FileCode" />
                    </TraceIcon>
                    <TraceTitle>
                      {`${suite.name}, ${namedTestResultsSuitesFormatCounts(counts)}`}
                    </TraceTitle>
                  </TraceLine>
                }
              >
                <Trace>
                  {suite.tests.map((test) => (
                    <NamedTestResultsSuitesTestRow
                      key={test.name}
                      test={test}
                    />
                  ))}
                </Trace>
              </TracePanel>
            );
          })}
        </Trace>
      </TracePanel>
    </Trace>
  );
}
export default function AiTestResultsExample() {
  return (
    <div style={frame}>
      <TestResultsSuitesExample />
    </div>
  );
}

Components and API

This composition has no separate component API. Use the APIs for its installed components.

ComponentTarget reference
trace Radix UI API
code-block Radix UI API
icon Radix UI API
button Radix UI API
message Radix UI API
bubble Radix UI API