Test Results
Test run summary with progress, suites and failing test details.
AssertionError: expected 0.02 to be less than 0.02
AssertionError: expected 0.02 to be less than 0.02
AssertionError: expected 0.02 to be less than 0.02
import { Test, TestDuration, TestError, TestErrorMessage, TestName, TestResults, TestResultsContent, TestResultsDuration, TestResultsHeader, TestResultsProgress, TestResultsSummary, TestStatus, TestSuite, TestSuiteContent, TestSuiteName,} from "@/components/ai/test-results";
export default function Demo() { return ( <TestResults className="max-w-xl" summary={{ duration: 1840, failed: 1, passed: 7, skipped: 1, total: 9 }} > <TestResultsHeader> <TestResultsSummary /> <TestResultsDuration /> </TestResultsHeader> <TestResultsProgress /> <TestResultsContent> <TestSuite defaultOpen name="keeper.test.ts" status="failed"> <TestSuiteName /> <TestSuiteContent> <Test duration={12} name="skips when drift is under the limit" status="passed" /> <Test duration={31} name="rebalances at exactly 2%" status="failed"> <TestStatus /> <TestName /> <TestDuration /> <TestError> <TestErrorMessage> AssertionError: expected 0.02 to be less than 0.02 </TestErrorMessage> </TestError> </Test> <Test name="respects approval above $500" status="skipped" /> <Test name="sends one transaction" status="running" /> </TestSuiteContent> </TestSuite> </TestResultsContent> </TestResults> );}<script setup lang="ts">import { Test, TestDuration, TestError, TestErrorMessage, TestName, TestResults, TestResultsContent, TestResultsDuration, TestResultsHeader, TestResultsProgress, TestResultsSummary, TestStatus, TestSuite, TestSuiteContent, TestSuiteName,} from "@/components/ai/test-results";
const summary = { duration: 1840, failed: 1, passed: 7, skipped: 1, total: 9 };</script>
<template> <TestResults class="max-w-xl" :summary="summary"> <TestResultsHeader> <TestResultsSummary /> <TestResultsDuration /> </TestResultsHeader> <TestResultsProgress /> <TestResultsContent> <TestSuite default-open name="keeper.test.ts" status="failed"> <TestSuiteName /> <TestSuiteContent> <Test :duration="12" name="skips when drift is under the limit" status="passed" /> <Test :duration="31" name="rebalances at exactly 2%" status="failed"> <TestStatus /> <TestName /> <TestDuration /> <TestError> <TestErrorMessage>AssertionError: expected 0.02 to be less than 0.02</TestErrorMessage> </TestError> </Test> <Test name="respects approval above $500" status="skipped" /> <Test name="sends one transaction" status="running" /> </TestSuiteContent> </TestSuite> </TestResultsContent> </TestResults></template><script lang="ts"> import { Test, TestDuration, TestError, TestErrorMessage, TestName, TestResults, TestResultsContent, TestResultsDuration, TestResultsHeader, TestResultsProgress, TestResultsSummary, TestStatus, TestSuite, TestSuiteContent, TestSuiteName, } from "$lib/components/ai/test-results";
const summary = { duration: 1840, failed: 1, passed: 7, skipped: 1, total: 9 };</script>
<TestResults class="max-w-xl" {summary}> <TestResultsHeader> <TestResultsSummary /> <TestResultsDuration /> </TestResultsHeader> <TestResultsProgress /> <TestResultsContent> <TestSuite open name="keeper.test.ts" status="failed"> <TestSuiteName /> <TestSuiteContent> <Test duration={12} name="skips when drift is under the limit" status="passed" /> <Test duration={31} name="rebalances at exactly 2%" status="failed"> <TestStatus /> <TestName /> <TestDuration /> <TestError> <TestErrorMessage>AssertionError: expected 0.02 to be less than 0.02</TestErrorMessage> </TestError> </Test> <Test name="respects approval above $500" status="skipped" /> <Test name="sends one transaction" status="running" /> </TestSuiteContent> </TestSuite> </TestResultsContent></TestResults>Built on collapsible and badge.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-test-resultspnpm dlx shadcn@latest add @edmi-ui/ai-test-resultsyarn dlx shadcn@latest add @edmi-ui/ai-test-resultsbunx shadcn@latest add @edmi-ui/ai-test-resultsPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
badge,collapsible.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/ai-test-resultspnpm dlx shadcn-vue@latest add @edmi-ui/ai-test-resultsyarn dlx shadcn-vue@latest add @edmi-ui/ai-test-resultsbunx shadcn-vue@latest add @edmi-ui/ai-test-resultsPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @lucide/vue @vueuse/coreTerminal window pnpm add @lucide/vue @vueuse/coreTerminal window yarn add @lucide/vue @vueuse/coreTerminal window bun add @lucide/vue @vueuse/coreAdd the Edmi components this one builds on:
badge,collapsible.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-test-results.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-test-results.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-test-results.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-test-results.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
badge,collapsible.Copy the code from Component source into your project and update the import paths to match your aliases.
import { TestResults, TestResultsDuration, TestResultsHeader, TestResultsProgress, TestResultsSummary,} from "@/components/ai/test-results";
<TestResults className="max-w-xl" summary={{ duration: 1840, failed: 1, passed: 7, skipped: 1, total: 9 }}> <TestResultsHeader> <TestResultsSummary /> <TestResultsDuration /> </TestResultsHeader> <TestResultsProgress /> {/* … */}</TestResults><script setup lang="ts">import { Test, TestDuration, TestError, TestErrorMessage, TestName, TestResults, TestResultsContent, TestResultsDuration, TestResultsHeader, TestResultsProgress, TestResultsSummary, TestStatus, TestSuite, TestSuiteContent, TestSuiteName,} from "@/components/ai/test-results";</script>
<template> <TestResults class="max-w-xl"> <TestResultsHeader> <TestResultsSummary /> <TestResultsDuration /> </TestResultsHeader> <TestResultsProgress /> <TestResultsContent> <TestSuite default-open name="keeper.test.ts" status="failed"> <TestSuiteName /> <TestSuiteContent> <Test :duration="12" name="skips when drift is under the limit" status="passed" /> <Test :duration="31" name="rebalances at exactly 2%" status="failed"> <TestStatus /> <TestName /> <TestDuration /> <TestError> <TestErrorMessage>AssertionError: expected 0.02 to be less than 0.02</TestErrorMessage> </TestError> </Test> <Test name="respects approval above $500" status="skipped" /> <Test name="sends one transaction" status="running" /> </TestSuiteContent> </TestSuite> </TestResultsContent> </TestResults></template><script lang="ts"> import { Test, TestDuration, TestError, TestErrorMessage, TestName, TestResults, TestResultsContent, TestResultsDuration, TestResultsHeader, TestResultsProgress, TestResultsSummary, TestStatus, TestSuite, TestSuiteContent, TestSuiteName, } from "$lib/components/ai/test-results";</script>
<TestResults class="max-w-xl" > <TestResultsHeader> <TestResultsSummary /> <TestResultsDuration /> </TestResultsHeader> <TestResultsProgress /> <TestResultsContent> <TestSuite open name="keeper.test.ts" status="failed"> <TestSuiteName /> <TestSuiteContent> <Test duration={12} name="skips when drift is under the limit" status="passed" /> <Test duration={31} name="rebalances at exactly 2%" status="failed"> <TestStatus /> <TestName /> <TestDuration /> <TestError> <TestErrorMessage>AssertionError: expected 0.02 to be less than 0.02</TestErrorMessage> </TestError> </Test> <Test name="respects approval above $500" status="skipped" /> <Test name="sends one transaction" status="running" /> </TestSuiteContent> </TestSuite> </TestResultsContent></TestResults>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import type { ComponentProps, HTMLAttributes, ReactNode } from "react";import { createContext, useContext, useMemo } from "react";import { CheckCircleIcon, CaretDownIcon } from "@phosphor-icons/react";import { Badge } from "@/registry/edmi/ui/badge";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@/registry/edmi/ui/collapsible";
type TestStatus = "passed" | "failed" | "skipped" | "running";
interface TestResultsSummary { passed: number; failed: number; skipped: number; total: number; duration?: number;}
interface TestResultsContextType { summary?: TestResultsSummary;}
const TestResultsContext = createContext<TestResultsContextType>({});
const formatDuration = (ms: number) => { if (ms < 1000) { return `${ms}ms`; } return `${(ms / 1000).toFixed(2)} s`;};
const statusStyles: Record<TestStatus, string> = { failed: "text-destructive-text", passed: "text-success-text", running: "text-info-text", skipped: "text-warning-text",};
const statusBadgeVariants: Record< TestStatus, ComponentProps<typeof Badge>["variant"]> = { failed: "destructive", passed: "success", running: "info", skipped: "warning",};
const statusIcons: Record<TestStatus, ReactNode> = { failed: ( <svg aria-hidden="true" className="size-3.5" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.7" viewBox="0 0 24 24" > <circle cx="12" cy="12" r="9" /> <path d="m9 9 6 6M15 9l-6 6" /> </svg> ), passed: ( <CheckCircleIcon className="size-3.5" /> ), running: ( <svg aria-hidden="true" className="size-3.5 animate-spin" fill="none" viewBox="0 0 24 24" > <circle cx="12" cy="12" r="9" stroke="currentColor" strokeOpacity=".22" strokeWidth="2" /> <path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeLinecap="round" strokeWidth="2" /> </svg> ), skipped: ( <svg aria-hidden="true" className="size-3.5" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.7" viewBox="0 0 24 24" > <circle cx="12" cy="12" r="8" /> </svg> ),};
export type TestResultsHeaderProps = HTMLAttributes<HTMLDivElement>;
export const TestResultsHeader = ({ className, children, ...props}: TestResultsHeaderProps) => ( <div data-slot="ai-test-results-header" className={cn( "flex items-center justify-between gap-2.5 px-4 py-3.5", className, )} {...props} > {children} </div>);
export type TestResultsDurationProps = HTMLAttributes<HTMLSpanElement>;
export const TestResultsDuration = ({ className, children, ...props}: TestResultsDurationProps) => { const { summary } = useContext(TestResultsContext);
if (!summary?.duration) { return null; }
return ( <span className={cn("font-mono text-xs text-muted-foreground", className)} {...props} > {children ?? formatDuration(summary.duration)} </span> );};
export type TestResultsSummaryProps = HTMLAttributes<HTMLDivElement>;
export const TestResultsSummary = ({ className, children, ...props}: TestResultsSummaryProps) => { const { summary } = useContext(TestResultsContext);
if (!summary) { return null; }
return ( <div className={cn("flex items-center gap-2.5", className)} {...props}> {children ?? ( <> <span className="font-semibold">Tests</span> <Badge className="h-5" variant="success"> {summary.passed} passed </Badge> {summary.failed > 0 && ( <Badge className="h-5" variant="destructive"> {summary.failed} failed </Badge> )} {summary.skipped > 0 && ( <Badge className="h-5" variant="warning"> {summary.skipped} skipped </Badge> )} </> )} </div> );};
export type TestResultsProps = HTMLAttributes<HTMLDivElement> & { summary?: TestResultsSummary;};
export const TestResults = ({ summary, className, children, ...props}: TestResultsProps) => { const contextValue = useMemo(() => ({ summary }), [summary]);
return ( <TestResultsContext.Provider value={contextValue}> <div data-slot="ai-test-results" className={cn( "overflow-hidden rounded-xl border border-border bg-card", className, )} {...props} > {children ?? (summary && ( <> <TestResultsHeader> <TestResultsSummary /> <TestResultsDuration /> </TestResultsHeader> <TestResultsProgress /> </> ))} </div> </TestResultsContext.Provider> );};
export type TestResultsProgressProps = HTMLAttributes<HTMLDivElement>;
export const TestResultsProgress = ({ className, children, ...props}: TestResultsProgressProps) => { const { summary } = useContext(TestResultsContext);
if (!summary) { return null; }
const running = Math.max( summary.total - summary.passed - summary.failed - summary.skipped, 0, );
return ( <div data-slot="ai-test-results-progress" className={cn("-mt-0.5 px-4 pb-3.5", className)} {...props} > {children ?? ( <div aria-label={`${summary.passed} of ${summary.total} tests passed`} className="flex h-1.5 gap-0.5" role="img" > {summary.passed > 0 && ( <span className="rounded-[3px] bg-success" style={{ flex: summary.passed }} /> )} {summary.failed > 0 && ( <span className="rounded-[3px] bg-destructive" style={{ flex: summary.failed }} /> )} {summary.skipped > 0 && ( <span className="rounded-[3px] bg-warning" style={{ flex: summary.skipped }} /> )} {running > 0 && ( <span className="rounded-[3px] bg-muted" style={{ flex: running }} /> )} </div> )} </div> );};
export type TestResultsContentProps = HTMLAttributes<HTMLDivElement>;
export const TestResultsContent = ({ className, children, ...props}: TestResultsContentProps) => ( <div data-slot="ai-test-results-content" className={cn( "space-y-1 border-t border-border-2 px-4 pt-1.5 pb-3", className, )} {...props} > {children} </div>);
interface TestSuiteContextType { name: string; status: TestStatus;}
const TestSuiteContext = createContext<TestSuiteContextType>({ name: "", status: "passed",});
export type TestSuiteProps = ComponentProps<typeof Collapsible> & { name: string; status: TestStatus;};
export const TestSuite = ({ name, status, className, children, ...props}: TestSuiteProps) => { const contextValue = useMemo(() => ({ name, status }), [name, status]);
return ( <TestSuiteContext.Provider value={contextValue}> <Collapsible data-slot="ai-test-suite" className={className} {...props}> {children} </Collapsible> </TestSuiteContext.Provider> );};
export type TestSuiteNameProps = ComponentProps<typeof CollapsibleTrigger>;
export const TestSuiteName = ({ className, children, ...props}: TestSuiteNameProps) => { const { name, status } = useContext(TestSuiteContext);
return ( <CollapsibleTrigger data-slot="ai-test-suite-name" className={cn( "group/ai-test-suite flex w-full items-center gap-2 py-1.5 text-left outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", className, )} {...props} > <CaretDownIcon className="size-3.5 shrink-0 text-muted-foreground transition-transform group-data-[panel-open]/ai-test-suite:rotate-180" /> <span className="font-mono text-[12.5px]">{children ?? name}</span> <Badge className="h-[18px] text-[10.5px]" variant={statusBadgeVariants[status]} > {status} </Badge> </CollapsibleTrigger> );};
export type TestSuiteStatsProps = HTMLAttributes<HTMLDivElement> & { passed?: number; failed?: number; skipped?: number;};
export const TestSuiteStats = ({ passed = 0, failed = 0, skipped = 0, className, children, ...props}: TestSuiteStatsProps) => ( <div className={cn("ml-auto flex items-center gap-2 text-xs", className)} {...props} > {children ?? ( <> {passed > 0 && ( <span className="text-success-text">{passed} passed</span> )} {failed > 0 && ( <span className="text-destructive-text">{failed} failed</span> )} {skipped > 0 && ( <span className="text-warning-text">{skipped} skipped</span> )} </> )} </div>);
export type TestSuiteContentProps = ComponentProps<typeof CollapsibleContent>;
export const TestSuiteContent = ({ className, children, ...props}: TestSuiteContentProps) => ( <CollapsibleContent data-slot="ai-test-suite-content" className={className} {...props} > <div>{children}</div> </CollapsibleContent>);
interface TestContextType { name: string; status: TestStatus; duration?: number;}
const TestContext = createContext<TestContextType>({ name: "", status: "passed",});
export type TestNameProps = HTMLAttributes<HTMLSpanElement>;
export const TestName = ({ className, children, ...props }: TestNameProps) => { const { name } = useContext(TestContext);
return ( <span className={cn("flex-1", className)} {...props}> {children ?? name} </span> );};
export type TestDurationProps = HTMLAttributes<HTMLSpanElement>;
export const TestDuration = ({ className, children, ...props}: TestDurationProps) => { const { duration } = useContext(TestContext);
if (duration === undefined) { return null; }
return ( <span className={cn( "ml-auto font-mono text-xs text-muted-foreground", className, )} {...props} > {children ?? `${duration}ms`} </span> );};
export type TestStatusProps = HTMLAttributes<HTMLSpanElement>;
export const TestStatus = ({ className, children, ...props}: TestStatusProps) => { const { status } = useContext(TestContext);
return ( <span className={cn("inline-flex shrink-0", statusStyles[status], className)} {...props} > {children ?? statusIcons[status]} </span> );};
export type TestProps = HTMLAttributes<HTMLDivElement> & { name: string; status: TestStatus; duration?: number;};
export const Test = ({ name, status, duration, className, children, ...props}: TestProps) => { const contextValue = useMemo( () => ({ duration, name, status }), [duration, name, status], );
return ( <TestContext.Provider value={contextValue}> <div data-slot="ai-test" data-status={status} className={cn( "flex flex-wrap items-center gap-x-2 py-[5px] text-[12.5px]", className, )} {...props} > {children ?? ( <> <TestStatus /> <TestName /> {duration !== undefined && <TestDuration />} </> )} </div> </TestContext.Provider> );};
export type TestErrorProps = HTMLAttributes<HTMLDivElement>;
export const TestError = ({ className, children, ...props}: TestErrorProps) => ( <div data-slot="ai-test-error" className={cn( "mt-1 mb-1.5 ml-[22px] basis-full rounded-md bg-destructive-soft px-3 py-2.5 font-mono text-xs leading-[1.6] text-destructive-text", className, )} {...props} > {children} </div>);
export type TestErrorMessageProps = HTMLAttributes<HTMLParagraphElement>;
export const TestErrorMessage = ({ className, children, ...props}: TestErrorMessageProps) => ( <p className={cn("m-0", className)} {...props}> {children} </p>);
export type TestErrorStackProps = HTMLAttributes<HTMLPreElement>;
export const TestErrorStack = ({ className, children, ...props}: TestErrorStackProps) => ( <pre className={cn("mt-2 overflow-auto font-mono text-xs", className)} {...props} > {children} </pre>);<script setup lang="ts">import type { HTMLAttributes } from "vue"import type { TestStatusType } from "./context"import { computed, provide, reactive } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { TestContextKey } from "./context"import TestDuration from "./TestDuration.vue"import TestName from "./TestName.vue"import TestStatus from "./TestStatus.vue"
const props = defineProps<{ name: string status: TestStatusType duration?: number class?: HTMLAttributes["class"]}>()
const context = reactive({ name: computed(() => props.name), status: computed(() => props.status), duration: computed(() => props.duration),})
provide(TestContextKey, context)</script>
<template> <div data-slot="ai-test" :data-status="props.status" :class="cn('flex flex-wrap items-center gap-x-2 py-[5px] text-[12.5px]', props.class)" > <slot> <TestStatus /> <TestName /> <TestDuration v-if="props.duration !== undefined" /> </slot> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { useTestContext } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const context = useTestContext()</script>
<template> <span v-if="context.duration !== undefined" :class="cn('ml-auto font-mono text-xs text-muted-foreground', props.class)" > <slot>{{ context.duration }}ms</slot> </span></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div data-slot="ai-test-error" :class="cn( 'mt-1 mb-1.5 ml-[22px] basis-full rounded-md bg-destructive-soft px-3 py-2.5 font-mono text-xs leading-[1.6] text-destructive-text', props.class, )" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <p :class="cn('m-0', props.class)"> <slot /> </p></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <pre :class="cn('mt-2 overflow-auto font-mono text-xs', props.class)"><slot /></pre></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { useTestContext } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { name } = useTestContext()</script>
<template> <span :class="cn('flex-1', props.class)"> <slot>{{ name }}</slot> </span></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import type { TestResultsSummaryData } from "./context"import { computed, provide, reactive } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { TestResultsContextKey } from "./context"import TestResultsDuration from "./TestResultsDuration.vue"import TestResultsHeader from "./TestResultsHeader.vue"import TestResultsProgress from "./TestResultsProgress.vue"import TestResultsSummary from "./TestResultsSummary.vue"
const props = defineProps<{ summary?: TestResultsSummaryData class?: HTMLAttributes["class"]}>()
const context = reactive({ summary: computed(() => props.summary),})
provide(TestResultsContextKey, context)</script>
<template> <div data-slot="ai-test-results" :class="cn('overflow-hidden rounded-xl border border-border bg-card', props.class)" > <slot> <template v-if="props.summary"> <TestResultsHeader> <TestResultsSummary /> <TestResultsDuration /> </TestResultsHeader> <TestResultsProgress /> </template> </slot> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div data-slot="ai-test-results-content" :class="cn('space-y-1 border-t border-border-2 px-4 pt-1.5 pb-3', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { formatDuration, useTestResultsContext } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const context = useTestResultsContext()</script>
<template> <span v-if="context.summary?.duration" :class="cn('font-mono text-xs text-muted-foreground', props.class)" > <slot>{{ formatDuration(context.summary.duration) }}</slot> </span></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <div data-slot="ai-test-results-header" :class="cn('flex items-center justify-between gap-2.5 px-4 py-3.5', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { useTestResultsContext } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const context = useTestResultsContext()
const running = computed(() => { const s = context.summary return s ? Math.max(s.total - s.passed - s.failed - s.skipped, 0) : 0})</script>
<template> <div v-if="context.summary" data-slot="ai-test-results-progress" :class="cn('-mt-0.5 px-4 pb-3.5', props.class)" > <slot> <div class="flex h-1.5 gap-0.5" role="img" :aria-label="`${context.summary.passed} of ${context.summary.total} tests passed`" > <span v-if="context.summary.passed > 0" class="rounded-[3px] bg-success" :style="{ flex: context.summary.passed }" /> <span v-if="context.summary.failed > 0" class="rounded-[3px] bg-destructive" :style="{ flex: context.summary.failed }" /> <span v-if="context.summary.skipped > 0" class="rounded-[3px] bg-warning" :style="{ flex: context.summary.skipped }" /> <span v-if="running > 0" class="rounded-[3px] bg-muted" :style="{ flex: running }" /> </div> </slot> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Badge } from "@/registry/edmi/ui/badge"import { useTestResultsContext } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const context = useTestResultsContext()</script>
<template> <div v-if="context.summary" :class="cn('flex items-center gap-2.5', props.class)"> <slot> <span class="font-semibold">Tests</span> <Badge class="h-5" variant="success"> {{ context.summary.passed }} passed </Badge> <Badge v-if="context.summary.failed > 0" class="h-5" variant="destructive"> {{ context.summary.failed }} failed </Badge> <Badge v-if="context.summary.skipped > 0" class="h-5" variant="warning"> {{ context.summary.skipped }} skipped </Badge> </slot> </div></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { CircleCheckIcon } from "@lucide/vue"import { cn } from "@/registry/edmi/lib/utils"import { statusStyles, useTestContext } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { status } = useTestContext()</script>
<template> <span :class="cn('inline-flex shrink-0', statusStyles[status], props.class)"> <slot> <CircleCheckIcon v-if="status === 'passed'" class="size-3.5" /> <svg v-else-if="status === 'failed'" aria-hidden="true" class="size-3.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.7" viewBox="0 0 24 24" > <circle cx="12" cy="12" r="9" /> <path d="m9 9 6 6M15 9l-6 6" /> </svg> <svg v-else-if="status === 'running'" aria-hidden="true" class="size-3.5 animate-spin" fill="none" viewBox="0 0 24 24" > <circle cx="12" cy="12" r="9" stroke="currentColor" stroke-opacity=".22" stroke-width="2" /> <path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" stroke-linecap="round" stroke-width="2" /> </svg> <svg v-else aria-hidden="true" class="size-3.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.7" viewBox="0 0 24 24" > <circle cx="12" cy="12" r="8" /> </svg> </slot> </span></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import type { TestStatusType } from "./context"import { useVModel } from "@vueuse/core"import { computed, provide, reactive } from "vue"import { Collapsible } from "@/registry/edmi/ui/collapsible"import { TestSuiteContextKey } from "./context"
const props = withDefaults(defineProps<{ name: string status: TestStatusType defaultOpen?: boolean modelValue?: boolean class?: HTMLAttributes["class"]}>(), { defaultOpen: false, modelValue: undefined,})
const emit = defineEmits<{ (e: "update:modelValue", value: boolean): void}>()
const isOpen = useVModel(props, "modelValue", emit, { defaultValue: props.defaultOpen, passive: true,})
const context = reactive({ name: computed(() => props.name), status: computed(() => props.status),})
provide(TestSuiteContextKey, context)</script>
<template> <Collapsible v-model:open="isOpen" data-slot="ai-test-suite" :class="props.class"> <slot /> </Collapsible></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { CollapsibleContent } from "@/registry/edmi/ui/collapsible"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <CollapsibleContent data-slot="ai-test-suite-content" :class="props.class"> <div> <slot /> </div> </CollapsibleContent></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { ChevronDownIcon } from "@lucide/vue"import { cn } from "@/registry/edmi/lib/utils"import { Badge } from "@/registry/edmi/ui/badge"import { CollapsibleTrigger } from "@/registry/edmi/ui/collapsible"import { useTestSuiteContext } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { name, status } = useTestSuiteContext()
const variants = { failed: "destructive", passed: "success", running: "info", skipped: "warning",} as const</script>
<template> <CollapsibleTrigger data-slot="ai-test-suite-name" :class="cn( 'group/ai-test-suite flex w-full items-center gap-2 py-1.5 text-left outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring', props.class, )" > <ChevronDownIcon class="size-3.5 shrink-0 text-muted-foreground transition-transform group-data-[state=open]/ai-test-suite:rotate-180" /> <span class="font-mono text-[12.5px]"><slot>{{ name }}</slot></span> <Badge class="h-[18px] text-[10.5px]" :variant="variants[status]"> {{ status }} </Badge> </CollapsibleTrigger></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"
const props = withDefaults(defineProps<{ passed?: number failed?: number skipped?: number class?: HTMLAttributes["class"]}>(), { passed: 0, failed: 0, skipped: 0,})</script>
<template> <div :class="cn('ml-auto flex items-center gap-2 text-xs', props.class)"> <slot> <span v-if="props.passed > 0" class="text-success-text">{{ props.passed }} passed</span> <span v-if="props.failed > 0" class="text-destructive-text">{{ props.failed }} failed</span> <span v-if="props.skipped > 0" class="text-warning-text">{{ props.skipped }} skipped</span> </slot> </div></template>import type { InjectionKey } from "vue";import { inject } from "vue";
export type TestStatusType = "passed" | "failed" | "skipped" | "running";
export interface TestResultsSummaryData { passed: number; failed: number; skipped: number; total: number; duration?: number;}
export interface TestResultsContextType { summary?: TestResultsSummaryData;}
export interface TestSuiteContextType { name: string; status: TestStatusType;}
export interface TestContextType { name: string; status: TestStatusType; duration?: number;}
export const TestResultsContextKey: InjectionKey<TestResultsContextType> = Symbol("TestResultsContext");
export const TestSuiteContextKey: InjectionKey<TestSuiteContextType> = Symbol("TestSuiteContext");
export const TestContextKey: InjectionKey<TestContextType> = Symbol("TestContext");
export function useTestResultsContext() { const context = inject(TestResultsContextKey); if (!context) { throw new Error( "useTestResultsContext must be used within a <TestResults> component", ); } return context;}
export function useTestSuiteContext() { const context = inject(TestSuiteContextKey); if (!context) { throw new Error( "useTestSuiteContext must be used within a <TestSuite> component", ); } return context;}
export function useTestContext() { const context = inject(TestContextKey); if (!context) { throw new Error("useTestContext must be used within a <Test> component"); } return context;}
export const formatDuration = (ms: number) => { if (ms < 1000) { return `${ms}ms`; } return `${(ms / 1000).toFixed(2)} s`;};
export const statusStyles: Record<TestStatusType, string> = { failed: "text-destructive-text", passed: "text-success-text", running: "text-info-text", skipped: "text-warning-text",};export type { TestResultsSummaryData, TestStatusType,} from "./context";export { default as Test } from "./Test.vue";export { default as TestDuration } from "./TestDuration.vue";export { default as TestError } from "./TestError.vue";export { default as TestErrorMessage } from "./TestErrorMessage.vue";export { default as TestErrorStack } from "./TestErrorStack.vue";export { default as TestName } from "./TestName.vue";export { default as TestResults } from "./TestResults.vue";export { default as TestResultsContent } from "./TestResultsContent.vue";export { default as TestResultsDuration } from "./TestResultsDuration.vue";export { default as TestResultsHeader } from "./TestResultsHeader.vue";export { default as TestResultsProgress } from "./TestResultsProgress.vue";export { default as TestResultsSummary } from "./TestResultsSummary.vue";export { default as TestStatus } from "./TestStatus.vue";export { default as TestSuite } from "./TestSuite.vue";export { default as TestSuiteContent } from "./TestSuiteContent.vue";export { default as TestSuiteName } from "./TestSuiteName.vue";export { default as TestSuiteStats } from "./TestSuiteStats.vue";import Test from "./test.svelte";import TestDuration from "./test-duration.svelte";import TestError from "./test-error.svelte";import TestErrorMessage from "./test-error-message.svelte";import TestErrorStack from "./test-error-stack.svelte";import TestName from "./test-name.svelte";import TestResults from "./test-results.svelte";import TestResultsContent from "./test-results-content.svelte";import TestResultsDuration from "./test-results-duration.svelte";import TestResultsHeader from "./test-results-header.svelte";import TestResultsProgress from "./test-results-progress.svelte";import TestResultsSummary from "./test-results-summary.svelte";import TestStatus from "./test-status.svelte";import TestSuite from "./test-suite.svelte";import TestSuiteContent from "./test-suite-content.svelte";import TestSuiteName from "./test-suite-name.svelte";import TestSuiteStats from "./test-suite-stats.svelte";
export type { TestResultsSummaryData, TestStatusType,} from "./use-test-results.svelte.js";export { Test, TestDuration, TestError, TestErrorMessage, TestErrorStack, TestName, TestResults, TestResultsContent, TestResultsDuration, TestResultsHeader, TestResultsProgress, TestResultsSummary, TestStatus, TestSuite, TestSuiteContent, TestSuiteName, TestSuiteStats,};<script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import { useTestContext } from "./use-test-results.svelte.js";
let { class: className, children }: { class?: string; children?: Snippet } = $props();
const test = useTestContext();</script>
{#if test.duration !== undefined} <span class={cn("ml-auto font-mono text-xs text-muted-foreground", className)}> {#if children}{@render children()}{:else}{test.duration}ms{/if} </span>{/if}<script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte";
let { class: className, children }: { class?: string; children?: Snippet } = $props();</script>
<p class={cn("m-0", className)}>{@render children?.()}</p><script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte";
let { class: className, children }: { class?: string; children?: Snippet } = $props();</script>
<pre class={cn("mt-2 overflow-auto font-mono text-xs", className)}>{@render children?.()}</pre><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="ai-test-error" class={cn( "mt-1 mb-1.5 ml-[22px] basis-full rounded-md bg-destructive-soft px-3 py-2.5 font-mono text-xs leading-[1.6] text-destructive-text", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import { useTestContext } from "./use-test-results.svelte.js";
let { class: className, children }: { class?: string; children?: Snippet } = $props();
const test = useTestContext();</script>
<span class={cn("flex-1", className)}> {#if children}{@render children()}{:else}{test.name}{/if}</span><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="ai-test-results-content" class={cn("space-y-1 border-t border-border-2 px-4 pt-1.5 pb-3", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import { formatDuration, useTestResultsContext } from "./use-test-results.svelte.js";
let { class: className, children }: { class?: string; children?: Snippet } = $props();
const results = useTestResultsContext();</script>
{#if results.summary?.duration} <span class={cn("font-mono text-xs text-muted-foreground", className)}> {#if children}{@render children()}{:else}{formatDuration(results.summary.duration)}{/if} </span>{/if}<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="ai-test-results-header" class={cn("flex items-center justify-between gap-2.5 px-4 py-3.5", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import { useTestResultsContext } from "./use-test-results.svelte.js";
let { class: className, children }: { class?: string; children?: Snippet } = $props();
const results = useTestResultsContext();
const running = $derived.by(() => { const s = results.summary; return s ? Math.max(s.total - s.passed - s.failed - s.skipped, 0) : 0; });</script>
{#if results.summary} <div data-slot="ai-test-results-progress" class={cn("-mt-0.5 px-4 pb-3.5", className)}> {#if children} {@render children()} {:else} <div class="flex h-1.5 gap-0.5" role="img" aria-label={`${results.summary.passed} of ${results.summary.total} tests passed`} > {#if results.summary.passed > 0} <span class="rounded-[3px] bg-success" style={`flex: ${results.summary.passed}`}></span> {/if} {#if results.summary.failed > 0} <span class="rounded-[3px] bg-destructive" style={`flex: ${results.summary.failed}`}></span> {/if} {#if results.summary.skipped > 0} <span class="rounded-[3px] bg-warning" style={`flex: ${results.summary.skipped}`}></span> {/if} {#if running > 0} <span class="rounded-[3px] bg-muted" style={`flex: ${running}`}></span> {/if} </div> {/if} </div>{/if}<script lang="ts"> import { Badge } from "$lib/registry/ui/badge/index.js"; import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import { useTestResultsContext } from "./use-test-results.svelte.js";
let { class: className, children }: { class?: string; children?: Snippet } = $props();
const results = useTestResultsContext();</script>
{#if results.summary} <div class={cn("flex items-center gap-2.5", className)}> {#if children} {@render children()} {:else} <span class="font-semibold">Tests</span> <Badge class="h-5" variant="success">{results.summary.passed} passed</Badge> {#if results.summary.failed > 0} <Badge class="h-5" variant="destructive">{results.summary.failed} failed</Badge> {/if} {#if results.summary.skipped > 0} <Badge class="h-5" variant="warning">{results.summary.skipped} skipped</Badge> {/if} {/if} </div>{/if}<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements"; import TestResultsDuration from "./test-results-duration.svelte"; import TestResultsHeader from "./test-results-header.svelte"; import TestResultsProgress from "./test-results-progress.svelte"; import TestResultsSummary from "./test-results-summary.svelte"; import { setTestResultsContext, type TestResultsSummaryData } from "./use-test-results.svelte.js";
let { ref = $bindable(null), summary, class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { summary?: TestResultsSummaryData; } = $props();
setTestResultsContext({ get summary() { return summary; }, });</script>
<div bind:this={ref} data-slot="ai-test-results" class={cn("overflow-hidden rounded-xl border border-border bg-card", className)} {...restProps}> {#if children} {@render children()} {:else if summary} <TestResultsHeader> <TestResultsSummary /> <TestResultsDuration /> </TestResultsHeader> <TestResultsProgress /> {/if}</div><script lang="ts"> import CheckCircleIcon from "phosphor-svelte/lib/CheckCircle"; import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import { statusStyles, useTestContext } from "./use-test-results.svelte.js";
let { class: className, children }: { class?: string; children?: Snippet } = $props();
const test = useTestContext();</script>
<span class={cn("inline-flex shrink-0", statusStyles[test.status], className)}> {#if children} {@render children()} {:else if test.status === "passed"} <CheckCircleIcon class="size-3.5" /> {:else if test.status === "failed"} <svg aria-hidden="true" class="size-3.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.7" viewBox="0 0 24 24" > <circle cx="12" cy="12" r="9" /> <path d="m9 9 6 6M15 9l-6 6" /> </svg> {:else if test.status === "running"} <svg aria-hidden="true" class="size-3.5 animate-spin" fill="none" viewBox="0 0 24 24"> <circle cx="12" cy="12" r="9" stroke="currentColor" stroke-opacity=".22" stroke-width="2" /> <path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" stroke-linecap="round" stroke-width="2" /> </svg> {:else} <svg aria-hidden="true" class="size-3.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.7" viewBox="0 0 24 24" > <circle cx="12" cy="12" r="8" /> </svg> {/if}</span><script lang="ts"> import * as Collapsible from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), children, ...restProps }: ComponentProps<typeof Collapsible.Content> = $props();</script>
<Collapsible.Content bind:ref data-slot="ai-test-suite-content" {...restProps}> <div> {@render children?.()} </div></Collapsible.Content><script lang="ts"> import CaretDownIcon from "phosphor-svelte/lib/CaretDown"; import { Badge, type BadgeVariant } from "$lib/registry/ui/badge/index.js"; import * as Collapsible from "$lib/registry/ui/collapsible/index.js"; import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import { type TestStatusType, useTestSuiteContext } from "./use-test-results.svelte.js";
let { class: className, children }: { class?: string; children?: Snippet } = $props();
const suite = useTestSuiteContext();
const variants: Record<TestStatusType, BadgeVariant> = { failed: "destructive", passed: "success", running: "info", skipped: "warning", };</script>
<Collapsible.Trigger data-slot="ai-test-suite-name" class={cn( "group/ai-test-suite flex w-full items-center gap-2 py-1.5 text-left outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", className )}> <CaretDownIcon class="size-3.5 shrink-0 text-muted-foreground transition-transform group-data-[state=open]/ai-test-suite:rotate-180" /> <span class="font-mono text-[12.5px]"> {#if children}{@render children()}{:else}{suite.name}{/if} </span> <Badge class="h-[18px] text-[10.5px]" variant={variants[suite.status]}>{suite.status}</Badge></Collapsible.Trigger><script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte";
let { passed = 0, failed = 0, skipped = 0, class: className, children, }: { passed?: number; failed?: number; skipped?: number; class?: string; children?: Snippet; } = $props();</script>
<div class={cn("ml-auto flex items-center gap-2 text-xs", className)}> {#if children} {@render children()} {:else} {#if passed > 0}<span class="text-success-text">{passed} passed</span>{/if} {#if failed > 0}<span class="text-destructive-text">{failed} failed</span>{/if} {#if skipped > 0}<span class="text-warning-text">{skipped} skipped</span>{/if} {/if}</div><script lang="ts"> import * as Collapsible from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte"; import { setTestSuiteContext, type TestStatusType } from "./use-test-results.svelte.js";
let { ref = $bindable(null), open = $bindable(false), name, status, children, ...restProps }: ComponentProps<typeof Collapsible.Root> & { name: string; status: TestStatusType; } = $props();
setTestSuiteContext({ get name() { return name; }, get status() { return status; }, });</script>
<Collapsible.Root bind:ref bind:open data-slot="ai-test-suite" {...restProps}> {@render children?.()}</Collapsible.Root><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements"; import TestDuration from "./test-duration.svelte"; import TestName from "./test-name.svelte"; import TestStatus from "./test-status.svelte"; import { setTestContext, type TestStatusType } from "./use-test-results.svelte.js";
let { ref = $bindable(null), name, status, duration, class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { name: string; status: TestStatusType; duration?: number; } = $props();
setTestContext({ get name() { return name; }, get status() { return status; }, get duration() { return duration; }, });</script>
<div bind:this={ref} data-slot="ai-test" data-status={status} class={cn("flex flex-wrap items-center gap-x-2 py-[5px] text-[12.5px]", className)} {...restProps}> {#if children} {@render children()} {:else} <TestStatus /> <TestName /> {#if duration !== undefined} <TestDuration /> {/if} {/if}</div>import { getContext, setContext } from "svelte";
export type TestStatusType = "passed" | "failed" | "skipped" | "running";
export interface TestResultsSummaryData { passed: number; failed: number; skipped: number; total: number; duration?: number;}
export interface TestResultsContext { readonly summary: TestResultsSummaryData | undefined;}export interface TestSuiteContext { readonly name: string; readonly status: TestStatusType;}export interface TestContext { readonly name: string; readonly status: TestStatusType; readonly duration: number | undefined;}
const RESULTS = Symbol("ai-test-results");const SUITE = Symbol("ai-test-suite");const TEST = Symbol("ai-test");
function make<T>(key: symbol, label: string, parent: string) { return [ (ctx: T) => setContext(key, ctx), (): T => { const ctx = getContext<T | undefined>(key); if (!ctx) throw new Error(`${label} must be used within ${parent}`); return ctx; }, ] as const;}
export const [setTestResultsContext, useTestResultsContext] = make<TestResultsContext>(RESULTS, "TestResults components", "TestResults");export const [setTestSuiteContext, useTestSuiteContext] = make<TestSuiteContext>(SUITE, "TestSuite components", "TestSuite");export const [setTestContext, useTestContext] = make<TestContext>( TEST, "Test components", "Test",);
export const formatDuration = (ms: number) => ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(2)} s`;
export const statusStyles: Record<TestStatusType, string> = { failed: "text-destructive-text", passed: "text-success-text", running: "text-info-text", skipped: "text-warning-text",};| Part | Prop | Type | Default |
|---|---|---|---|
| TestResults | summary |
{ passed, failed, skipped, total, duration? } |
|
| TestSuite | name, status |
string, passed | failed | skipped | running |
|
| Test | name, status, duration |
string, status, number |
|
| TestResultsHeader, Summary, Duration, Progress, Content, TestSuiteName, TestSuiteStats, TestSuiteContent, TestError, TestErrorMessage, TestErrorStack |
✦ Edmi additions
Section titled “✦ Edmi additions”- Summary badges and the progress bar use
--success,--destructive,--warningand--info; a running test shows a spinner. - Durations are mono.