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 React Aria. Each example runs with local data.

Installation

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

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

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
                isExpanded={
                  open.run ??
                  suites.some((suite) =>
                    suite.tests.some((test) => test.status === "failed"),
                  )
                }
                onExpandedChange={(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>
                  <Trace>
                    {suites.map((suite) => (
                      <TraceCollapsible
                        key={suite.name}
                        isExpanded={
                          open[suite.name] ??
                          suite.tests.some((test) => test.status === "failed")
                        }
                        onExpandedChange={(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>
                          <Trace>
                            {suite.tests.map((test) =>
                              test.status === "failed" ? (
                                <TraceCollapsible
                                  key={test.name}
                                  isExpanded={
                                    open[test.name] ?? test.status === "failed"
                                  }
                                  onExpandedChange={(value) =>
                                    expand(test.name, value)
                                  }
                                >
                                  <TraceCollapsibleTrigger>
                                    <TraceLine status="error">
                                      <TraceIcon>
                                        <Icon name="X" />
                                      </TraceIcon>
                                      <TraceTitle>{test.name}</TraceTitle>
                                    </TraceLine>
                                  </TraceCollapsibleTrigger>
                                  <TraceCollapsibleContent>
                                    <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"
          onPress={replay}
          isDisabled={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 isExpanded={expanded} onExpandedChange={setOpen}>
      <TraceCollapsibleTrigger>
        <TraceLine status={status}>
          <TraceIcon>
            <Icon name={status === "error" ? "CircleAlert" : "Terminal"} />
          </TraceIcon>
          <TraceTitle>{title}</TraceTitle>
        </TraceLine>
      </TraceCollapsibleTrigger>
      <TraceCollapsibleContent>{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 defaultExpanded>
        <TraceCollapsibleTrigger>
          <TraceLine status={summary.failed > 0 ? "error" : undefined}>
            <TraceIcon>
              <Icon name="FlaskConical" />
            </TraceIcon>
            <TraceTitle>
              Ran {namedTestResultsSuitesFormatCounts(summary)}
            </TraceTitle>
          </TraceLine>
        </TraceCollapsibleTrigger>
        <TraceCollapsibleContent>
          <Trace>
            {namedTestResultsSuitesSuites.map((suite) => {
              const counts = namedTestResultsSuitesCountTests(suite.tests);
              return (
                <TraceCollapsible
                  key={suite.name}
                  defaultExpanded={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>
                    <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 React Aria API
code-block React Aria API
icon React Aria API
button React Aria API
message React Aria API
bubble React Aria API