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 Base UI. Each example runs with local data.

Installation

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

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

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,
  TraceLine,
  TraceIcon,
  TraceTitle,
  TraceCollapsibleContent,
} 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,
};
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" ? (
              <TraceCollapsible
                open={
                  open.run ??
                  suites.some((suite) =>
                    suite.tests.some((test) => test.status === "failed"),
                  )
                }
                onOpenChange={(value) => expand("run", value)}
              >
                <TraceCollapsibleTrigger>
                  <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>
                </TraceCollapsibleTrigger>
                <TraceCollapsibleContent keepMounted>
                  <Trace>
                    {suites.map((suite) => (
                      <TraceCollapsible
                        key={suite.name}
                        open={
                          open[suite.name] ??
                          suite.tests.some((test) => test.status === "failed")
                        }
                        onOpenChange={(value) => expand(suite.name, value)}
                      >
                        <TraceCollapsibleTrigger>
                          <TraceLine
                            status={
                              suite.tests.some(
                                (test) => test.status === "failed",
                              )
                                ? "error"
                                : undefined
                            }
                          >
                            <TraceIcon>
                              <Icon name="FileCode" />
                            </TraceIcon>
                            <TraceTitle>
                              {suite.name}, {counts(suite.tests)}
                            </TraceTitle>
                          </TraceLine>
                        </TraceCollapsibleTrigger>
                        <TraceCollapsibleContent keepMounted>
                          <Trace>
                            {suite.tests.map((test) =>
                              test.status === "failed" ? (
                                <TraceCollapsible
                                  key={test.name}
                                  open={
                                    open[test.name] ?? test.status === "failed"
                                  }
                                  onOpenChange={(value) =>
                                    expand(test.name, value)
                                  }
                                >
                                  <TraceCollapsibleTrigger>
                                    <TraceLine status="error">
                                      <TraceIcon>
                                        <Icon name="X" />
                                      </TraceIcon>
                                      <TraceTitle>{test.name}</TraceTitle>
                                    </TraceLine>
                                  </TraceCollapsibleTrigger>
                                  <TraceCollapsibleContent keepMounted>
                                    <CodeBlock size="sm">
                                      <CodeBlockContent wrap>
                                        {test.error}
                                      </CodeBlockContent>
                                    </CodeBlock>
                                  </TraceCollapsibleContent>
                                </TraceCollapsible>
                              ) : (
                                <TraceLine key={test.name}>
                                  <TraceIcon>
                                    <Icon
                                      name={
                                        test.status === "skipped"
                                          ? "CircleDashed"
                                          : "Check"
                                      }
                                    />
                                  </TraceIcon>
                                  <TraceTitle>
                                    {test.status === "skipped"
                                      ? "Skipped "
                                      : ""}
                                    {test.name}
                                  </TraceTitle>
                                </TraceLine>
                              ),
                            )}
                          </Trace>
                        </TraceCollapsibleContent>
                      </TraceCollapsible>
                    ))}
                  </Trace>
                </TraceCollapsibleContent>
              </TraceCollapsible>
            ) : (
              <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 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 (
    <TraceCollapsible open={expanded} onOpenChange={setOpen}>
      <TraceCollapsibleTrigger>
        <TraceLine status={status}>
          <TraceIcon>
            <Icon name={status === "error" ? "CircleAlert" : "Terminal"} />
          </TraceIcon>
          <TraceTitle>{title}</TraceTitle>
        </TraceLine>
      </TraceCollapsibleTrigger>
      <TraceCollapsibleContent keepMounted>{children}</TraceCollapsibleContent>
    </TraceCollapsible>
  );
}
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,
  TraceLine,
  TraceIcon,
  TraceTitle,
  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 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 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 (
      <TraceCollapsible>
        <TraceCollapsibleTrigger>
          <TraceLine status="error">
            <TraceIcon>
              <Icon name="X" />
            </TraceIcon>
            <TraceTitle>{test.name}</TraceTitle>
          </TraceLine>
        </TraceCollapsibleTrigger>
        <TraceCollapsibleContent>
          <CodeBlock size="sm">
            <CodeBlockContent
              wrap
              style={{
                maxHeight: 256,
              }}
            >
              {test.error}
            </CodeBlockContent>
          </CodeBlock>
        </TraceCollapsibleContent>
      </TraceCollapsible>
    );
  }
  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>
      <TraceCollapsible defaultOpen>
        <TraceCollapsibleTrigger>
          <TraceLine status={summary.failed > 0 ? "error" : undefined}>
            <TraceIcon>
              <Icon name="FlaskConical" />
            </TraceIcon>
            <TraceTitle>
              Ran {namedTestResultsSuitesFormatCounts(summary)}
            </TraceTitle>
          </TraceLine>
        </TraceCollapsibleTrigger>
        <TraceCollapsibleContent keepMounted>
          <Trace>
            {namedTestResultsSuitesSuites.map((suite) => {
              const counts = namedTestResultsSuitesCountTests(suite.tests);
              return (
                <TraceCollapsible
                  key={suite.name}
                  defaultOpen={counts.failed > 0}
                >
                  <TraceCollapsibleTrigger>
                    <TraceLine status={counts.failed > 0 ? "error" : undefined}>
                      <TraceIcon>
                        <Icon name="FileCode" />
                      </TraceIcon>
                      <TraceTitle>
                        {`${suite.name}, ${namedTestResultsSuitesFormatCounts(counts)}`}
                      </TraceTitle>
                    </TraceLine>
                  </TraceCollapsibleTrigger>
                  <TraceCollapsibleContent keepMounted>
                    <Trace>
                      {suite.tests.map((test) => (
                        <NamedTestResultsSuitesTestRow
                          key={test.name}
                          test={test}
                        />
                      ))}
                    </Trace>
                  </TraceCollapsibleContent>
                </TraceCollapsible>
              );
            })}
          </Trace>
        </TraceCollapsibleContent>
      </TraceCollapsible>
    </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 Base UI API
code-block Base UI API
icon Base UI API
button Base UI API
message Base UI API
bubble Base UI API