Installation
Complete project setup with vue/reka, then install these components:
pnpm exec frontfriend download trace code-block icon button message bubble -f vue --primitive reka 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
Save as TestResultsExample.vue.
<script setup lang="ts">
import { ref, watch, onBeforeUnmount } from "vue";
import {
Trace,
TraceCollapsible,
TraceCollapsibleContent,
TraceCollapsibleTrigger,
TraceIcon,
TraceLine,
TraceTitle,
} from "@/components/ui/trace";
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block";
import { Icon } from "@/components/ui/icon";
import { Button } from "@/components/ui/button";
import { Message, MessageContent } from "@/components/ui/message";
import { Bubble, BubbleContent } from "@/components/ui/bubble";
type Phase = "complete" | "running" | "error" | "pending";
const props = withDefaults(defineProps<{ state?: Phase }>(), {
state: "complete",
});
const phase = ref<Phase>(props.state);
let timer: ReturnType<typeof setTimeout> | undefined;
watch(
() => props.state,
(value) => {
clearTimeout(timer);
phase.value = value;
},
);
onBeforeUnmount(() => clearTimeout(timer));
function replay() {
clearTimeout(timer);
expanded.value = {};
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
phase.value = "complete";
return;
}
phase.value = "running";
timer = setTimeout(() => {
phase.value = "complete";
}, 3600);
}
const expanded = ref<Record<string, boolean>>({});
function expand(id: string, value: boolean) {
expanded.value = { ...expanded.value, [id]: value };
}
interface 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` : ""}`;
}
</script>
<template>
<div
style="
width: 100%;
max-width: 384px;
min-width: 0;
min-height: 288px;
margin-inline: auto;
display: flex;
flex-direction: column;
gap: 16px;
padding-block: 24px;
font-size: var(--text-sm);
line-height: 1.5;
"
>
<Message align="end">
<MessageContent>
<Bubble align="end" variant="muted">
<BubbleContent>Run the checkout tests.</BubbleContent>
</Bubble>
</MessageContent>
</Message>
<Message>
<MessageContent style="gap: 16px; min-width: 0">
<Trace>
<TraceCollapsible
:unmount-on-hide="false"
v-if="phase === 'complete'"
:open="
expanded.run ??
suites.some((suite) =>
suite.tests.some((test) => test.status === 'failed'),
)
"
@update:open="expand('run', $event)"
>
<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>
<TraceCollapsible
:unmount-on-hide="false"
v-for="suite in suites"
:key="suite.name"
:open="
expanded[suite.name] ??
suite.tests.some((test) => test.status === 'failed')
"
@update:open="expand(suite.name, $event)"
>
<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>
<template v-for="test in suite.tests" :key="test.name">
<TraceCollapsible
:unmount-on-hide="false"
v-if="test.status === 'failed'"
:open="expanded[test.name] ?? true"
@update:open="expand(test.name, $event)"
>
<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 v-else>
<TraceIcon>
<Icon
:name="
test.status === 'skipped'
? 'CircleDashed'
: 'Check'
"
/>
</TraceIcon>
<TraceTitle>
{{ test.status === "skipped" ? "Skipped " : ""
}}{{ test.name }}
</TraceTitle>
</TraceLine>
</template>
</Trace>
</TraceCollapsibleContent>
</TraceCollapsible>
</Trace>
</TraceCollapsibleContent>
</TraceCollapsible>
<TraceLine v-else :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>
<p v-if="phase === 'complete'">
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"
:disabled="phase === 'running'"
@click="replay"
>
Replay
</Button>
</div>
</div>
</template> 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.
Save as AiTestResultsAdapter.vue.
<script setup lang="ts">
import type { UIMessage } from "ai";
import { computed, ref } from "vue";
import {
Trace,
TraceCollapsible,
TraceCollapsibleTrigger,
TraceCollapsibleContent,
TraceLine,
TraceTitle,
TraceIcon,
} from "@/components/ui/trace";
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block";
import { Icon } from "@/components/ui/icon";
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[] };
};
}
>;
type TestPart = Extract<
SdkTestMessage["parts"][number],
{ type: "tool-runTests" }
>;
const props = defineProps<{ message: SdkTestMessage; streaming: boolean }>();
const parts = computed(() =>
props.message.parts.filter(
(part): part is TestPart => part.type === "tool-runTests",
),
);
function counts(suites: SdkSuite[]) {
const tests = suites.flatMap((suite) => suite.tests);
return {
total: tests.length,
failed: tests.filter((test) => test.status === "failed").length,
skipped: tests.filter((test) => test.status === "skipped").length,
};
}
function summary(suites: SdkSuite[]) {
const { total, failed, skipped } = counts(suites);
return `${total} tests${failed ? `, ${failed} failed` : ""}${skipped ? `, ${skipped} skipped` : ""}`;
}
function state(part: TestPart) {
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 && !props.streaming);
return {
title: `${denied ? "Denied" : pending ? "Waiting to run" : failed ? "Failed to run" : "Running"} ${part.input?.command ?? "tests"}`,
status: pending
? "pending"
: failed
? "error"
: denied
? undefined
: "running",
} as const;
}
const open = ref<Record<string, boolean>>({});
function setOpen(id: string, value: boolean) {
open.value = { ...open.value, [id]: value };
}
</script>
<template>
<div
style="
width: 100%;
max-width: 384px;
min-width: 0;
margin-inline: auto;
display: grid;
gap: 16px;
padding-block: 24px;
font-size: var(--text-sm);
line-height: 1.5;
"
>
<Trace>
<template v-for="part in parts" :key="part.toolCallId">
<TraceCollapsible
:unmount-on-hide="false"
v-if="part.state === 'output-available' && !part.preliminary"
:open="open[part.toolCallId] ?? counts(part.output.suites).failed > 0"
@update:open="setOpen(part.toolCallId, $event)"
>
<TraceCollapsibleTrigger>
<TraceLine
:status="counts(part.output.suites).failed ? 'error' : undefined"
>
<TraceIcon>
<Icon name="FlaskConical" />
</TraceIcon>
<TraceTitle>Ran {{ summary(part.output.suites) }}</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<Trace>
<TraceCollapsible
:unmount-on-hide="false"
v-for="suite in part.output.suites"
:key="suite.id"
:open="
open[part.toolCallId + suite.id] ?? counts([suite]).failed > 0
"
@update:open="setOpen(part.toolCallId + suite.id, $event)"
>
<TraceCollapsibleTrigger>
<TraceLine
:status="counts([suite]).failed ? 'error' : undefined"
>
<TraceTitle>
{{ suite.name }}, {{ summary([suite]) }}
</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<Trace>
<template v-for="test in suite.tests" :key="test.id">
<TraceCollapsible
:unmount-on-hide="false"
v-if="test.error"
:open="
open[part.toolCallId + suite.id + test.id] ?? true
"
@update:open="
setOpen(part.toolCallId + suite.id + test.id, $event)
"
>
<TraceCollapsibleTrigger>
<TraceLine
:status="
test.status === 'failed' ? 'error' : undefined
"
>
<TraceTitle>{{ test.name }}</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<CodeBlock size="sm" variant="destructive">
<CodeBlockContent wrap>
{{ test.error }}
</CodeBlockContent>
</CodeBlock>
</TraceCollapsibleContent>
</TraceCollapsible>
<TraceLine
v-else
: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>
</template>
</Trace>
</TraceCollapsibleContent>
</TraceCollapsible>
</Trace>
</TraceCollapsibleContent>
</TraceCollapsible>
<template v-else>
<TraceLine :status="state(part).status">
<TraceIcon>
<Icon name="FlaskConical" />
</TraceIcon>
<TraceTitle>{{ state(part).title }}</TraceTitle>
</TraceLine>
<CodeBlock
v-if="part.state === 'output-error'"
size="sm"
variant="destructive"
>
<CodeBlockContent wrap>{{ part.errorText }}</CodeBlockContent>
</CodeBlock>
</template>
</template>
</Trace>
</div>
</template> Suites
Save as TestResultsSuitesExample.vue.
<script setup lang="ts">
import { Icon } from "@/components/ui/icon";
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block";
import {
Trace,
TraceCollapsible,
TraceCollapsibleContent,
TraceCollapsibleTrigger,
TraceIcon,
TraceLine,
TraceTitle,
} from "@/components/ui/trace";
interface Test {
name: string;
status: "passed" | "failed" | "skipped";
error?: string;
}
const suites: { 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 countTests(tests: Test[]) {
return {
total: tests.length,
failed: tests.filter((test) => test.status === "failed").length,
skipped: tests.filter((test) => test.status === "skipped").length,
};
}
function formatCounts({
total,
failed,
skipped,
}: ReturnType<typeof countTests>) {
return [
`${total} tests`,
failed > 0 && `${failed} failed`,
skipped > 0 && `${skipped} skipped`,
]
.filter(Boolean)
.join(", ");
}
const summary = countTests(suites.flatMap((suite) => suite.tests));
</script>
<template>
<div
style="
width: 100%;
max-width: 384px;
min-width: 0;
margin-inline: auto;
padding-block: 24px;
font-size: var(--text-sm);
line-height: 1.5;
"
>
<Trace>
<TraceCollapsible default-open :unmount-on-hide="false">
<TraceCollapsibleTrigger>
<TraceLine :status="summary.failed > 0 ? 'error' : undefined">
<TraceIcon>
<Icon name="FlaskConical" />
</TraceIcon>
<TraceTitle>Ran {{ formatCounts(summary) }}</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<Trace>
<TraceCollapsible
:unmount-on-hide="false"
v-for="suite in suites"
:key="suite.name"
:default-open="countTests(suite.tests).failed > 0"
>
<TraceCollapsibleTrigger>
<TraceLine
:status="
countTests(suite.tests).failed > 0 ? 'error' : undefined
"
>
<TraceIcon>
<Icon name="FileCode" />
</TraceIcon>
<TraceTitle>
{{ suite.name }},
{{ formatCounts(countTests(suite.tests)) }}
</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<Trace>
<template v-for="test in suite.tests" :key="test.name">
<TraceCollapsible
:unmount-on-hide="false"
v-if="test.status === 'failed'"
>
<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 v-else>
<TraceIcon>
<Icon
name="CircleDashed"
v-if="test.status === 'skipped'"
/>
<Icon name="Check" v-else />
</TraceIcon>
<TraceTitle>
{{
test.status === "skipped"
? `Skipped ${test.name}`
: test.name
}}
</TraceTitle>
</TraceLine>
</template>
</Trace>
</TraceCollapsibleContent>
</TraceCollapsible>
</Trace>
</TraceCollapsibleContent>
</TraceCollapsible>
</Trace>
</div>
</template> Components and API
This composition has no separate component API. Use the APIs for its installed components.
| Component | Target reference |
|---|---|
trace | Reka UI API |
code-block | Reka UI API |
icon | Reka UI API |
button | Reka UI API |
message | Reka UI API |
bubble | Reka UI API |