Test Results

Bir ajanın test çalıştırmasını olduğu anda gösterin. Testler dosyalarına akar, ardından her hata açık kalacak şekilde “118 passed, 2 failed in 4.2s” özetine katlanır. Kelime düzeyinde Expected ve Received farkları, zaman aşımları ve snapshot'lar, Stack Trace'ten gelen stack'ler, filtreler, başarısızları yeniden çalıştırma ve düzeltmesini isteme, ayrıca Vitest, Jest, Playwright ve pytest için adaptörler.

Bir ajan testlerinizi çalıştırdığında sohbet, bir geliştiricinin aradığını göstermeli: ne kadar ilerlediği, neyin başarısız olduğu ve neden. Testler çalışırken Test Results, rakam rakam ilerleyen sayaçlar ve geçen, başarısız ve atlanan olarak bölünmüş bir çubukla canlı bir “Running tests · 42 of 120” satırı gösterir. Testler bittikçe dosyalarına akar ve çalışan dosya açık kalır.

Çalıştırma bittiğinde satır, “118 passed, 2 failed in 4.2s” gibi geçmiş zamanlı bir özete dönüşür. Temiz bir çalıştırma katlanır. Başarısız olan açık kalır; geçen dosyalar katlanır, başarısız dosyalar açık kalır, siz kendiniz açıp kapatmadıysanız. Her hata mesajını, kelime düzeyinde bir Expected ve Received diff’ini ve Stack Trace üzerinden stack’ini gösterir; önce sizin frame’leriniz gelir, framework frame’leri katlanır. Zaman aşımları ve snapshot uyuşmazlıkları kendi ifadelerini alır.

All, Failed, Skipped ya da Slow ile filtreleyin, bir hatayı metin olarak kopyalayın, frame’leri editörünüzde açın ve Ask to fix, Rerun failed, Rerun ile Update snapshot’ı ajana geri gönderin. fromVitestJson, fromJestJson, fromPlaywrightJson ve fromJUnitXml raporları bir çalıştırmaya dönüştürür, applyTestEvent de canlı olayları tek bir çalıştırmada toplar.

  1. HextaUI Pro'yu alın

    Tek bir ödeme, gelecekte eklenecekler dahil tüm blokların kilidini açar. Fiyatları görün.

  2. Pro registry'yi components.json dosyasına ekleyin

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  3. Token'ınızı ekleyin

    Hesap sayfanızda bir token oluşturun ve .env.local dosyasına HEXTAUI_PRO_TOKEN olarak ekleyin.

  4. Bloğu ekleyin

    pnpm dlx shadcn@latest add @hextaui-pro/test-results

AI SDK ile

runTests’i, execute’u her reporter olayından sonra çalıştırmayı döndüren bir araç yapın; böylece her güncelleme ön çıktı olarak gelir. Testler başarısız olduğunda açık kalan bir ToolCall içinde gösterin ve Ask to fix ile Rerun failed’ı ajana mesaj olarak geri gönderin.

import { useChat } from "@ai-sdk/react"
import { isToolUIPart, tool, type ToolUIPart, type UIMessage } from "ai"
import { startVitest, type Reporter, type TestCase } from "vitest/node"
import { z } from "zod"

import { getEditorHref } from "../stack-trace/parse-stack-trace"
import { ToolCall } from "../tool-calls/tool-call"
import { getToolPartStatus } from "../tool-calls/tool-part"
import { TestResults } from "@/components/blocks/test-results/test-results"
import {
  applyTestEvent,
  createTestRun,
  formatFailures,
  formatRunForModel,
  getFailedTests,
  testKey,
  type TestEvent,
  type TestRun,
} from "@/components/blocks/test-results/test-run"

function suiteOf(test: TestCase) {
  const names: string[] = []
  for (let parent = test.parent; parent.type === "suite"; parent = parent.parent) {
    names.unshift(parent.name)
  }
  return names
}

export const runTests = tool({
  description:
    "Run the project's Vitest tests. Pass files or a test name pattern to run only some of them, for example the ones that failed.",
  inputSchema: z.object({
    files: z.array(z.string()).optional(),
    testNamePattern: z.string().optional(),
  }),
  execute: async function* ({ files = [], testNamePattern }, { abortSignal }) {
    const queue: TestEvent[] = []
    let finished = false
    let wake = () => {}
    const push = (event: TestEvent) => {
      queue.push(event)
      wake()
    }
    const describe = (test: TestCase) => ({
      file: test.module.relativeModuleId,
      suite: suiteOf(test),
      name: test.name,
      id: testKey(test.module.relativeModuleId, suiteOf(test), test.name),
    })
    const reporter: Reporter = {
      onTestRunStart: () => push({ type: "run-start", at: Date.now() }),
      onTestModuleCollected: (module) =>
        push({
          type: "file-start",
          file: module.relativeModuleId,
          total: [...module.children.allTests()].length,
        }),
      onTestCaseReady: (test) => push({ type: "test-start", ...describe(test) }),
      onTestCaseResult: (test) => {
        const result = test.result()
        push({
          type: "test-end",
          ...describe(test),
          status: result.state === "pending" ? "skipped" : result.state,
          duration: test.diagnostic()?.duration,
          retries: test.diagnostic()?.retryCount,
          error: result.errors?.[0],
        })
      },
      onTestModuleEnd: (module) =>
        push({
          type: "file-end",
          file: module.relativeModuleId,
          duration: module.diagnostic().duration,
          error: module.errors()[0],
        }),
      onTestRunEnd: (_modules, _errors, reason) => {
        push({ type: "run-end", at: Date.now(), cancelled: reason === "interrupted" })
        finished = true
      },
    }

    const vitest = await startVitest("test", files, {
      watch: false,
      reporters: [reporter],
      testNamePattern,
    })
    abortSignal?.addEventListener("abort", () => {
      void vitest.cancelCurrentRun("keyboard-input")
    })

    let run: TestRun = createTestRun({
      command: ["vitest run", ...files].join(" "),
    })
    while (!finished || queue.length) {
      if (!queue.length) {
        await new Promise<void>((resolve) => (wake = resolve))
      }
      while (queue.length) run = applyTestEvent(run, queue.shift()!)
      yield run
    }
    await vitest.close()
  },
  toModelOutput: ({ output }) => ({ type: "text", value: formatRunForModel(output) }),
})

function TestRunCall({
  part,
  streaming,
  onSend,
}: {
  part: ToolUIPart
  streaming: boolean
  onSend: (text: string) => void
}) {
  const run = part.state === "output-available" ? (part.output as TestRun) : undefined
  const failed = run ? getFailedTests(run) : []
  const input = (part.input ?? {}) as { files?: string[] }

  return (
    <ToolCall
      id={part.toolCallId}
      kind="run"
      subject={["vitest run", ...(input.files ?? [])].join(" ")}
      status={getToolPartStatus(part, { stopped: !streaming })}
      exitCode={run && run.status !== "running" ? (failed.length ? 1 : 0) : undefined}
      content={
        run ? (
          <TestResults
            run={run}
            getFrameHref={(frame) =>
              getEditorHref(frame, { root: process.env.NEXT_PUBLIC_PROJECT_ROOT })
            }
            onAskToFix={(tests) =>
              onSend(`Fix these failing tests:\n\n${formatFailures(tests)}`)
            }
            onRerunFailed={(tests) =>
              onSend(
                `Rerun only these tests: ${[...new Set(tests.map((test) => test.file))].join(", ")}`
              )
            }
            onRerunTest={({ file, test }) =>
              onSend(`Rerun “${test?.name}” in ${file}.`)
            }
          />
        ) : undefined
      }
    />
  )
}

function AssistantMessage({
  message,
  streaming,
  onSend,
}: {
  message: UIMessage
  streaming: boolean
  onSend: (text: string) => void
}) {
  return message.parts.map((part, index) =>
    isToolUIPart(part) && part.type === "tool-runTests" ? (
      <TestRunCall
        key={part.toolCallId}
        part={part}
        streaming={streaming}
        onSend={onSend}
      />
    ) : part.type === "text" ? (
      <p key={index}>{part.text}</p>
    ) : null
  )
}

export function Chat() {
  const { messages, status, sendMessage } = useChat()
  const busy = status === "submitted" || status === "streaming"

  return messages.map((message, index) =>
    message.role === "assistant" ? (
      <AssistantMessage
        key={message.id}
        message={message}
        streaming={busy && index === messages.length - 1}
        onSend={(text) => sendMessage({ text })}
      />
    ) : null
  )
}

Kaydedilmiş bir rapordan

fromVitestJson ve fromJestJson, JSON reporter’ın çıktısını okur. Mutlak yolları kısaltmak için bir root, frame’leri Cursor ya da VS Code’da açmak için getEditorHref verin.

"use client"

import { getEditorHref } from "../stack-trace/parse-stack-trace"
import { fromVitestJson, type VitestJsonReport } from "@/components/blocks/test-results/adapters"
import { TestResults } from "@/components/blocks/test-results/test-results"
import { formatFailures } from "@/components/blocks/test-results/test-run"

export function SavedRun({
  report,
  onSend,
}: {
  report: VitestJsonReport
  onSend: (text: string) => void
}) {
  const root = "/Users/me/acme"
  const run = fromVitestJson(report, { root })

  return (
    <TestResults
      run={run}
      root={root}
      getFrameHref={(frame) => getEditorHref(frame, { editor: "cursor", root })}
      onAskToFix={(failed) =>
        onSend(`Fix these failing tests:\n\n${formatFailures(failed)}`)
      }
    />
  )
}

pytest ve JUnit XML

fromJUnitXml, pytest --junitxml ve diğer JUnit raporlarını okur. Test sınıfları describe bloklarına dönüşür, hata veren kaynak kod olarak görünür ve Rerun failed size node id’lerini verir.

"use client"

import { fromJUnitXml } from "@/components/blocks/test-results/adapters"
import { TestResults } from "@/components/blocks/test-results/test-results"

export function PytestRun({
  xml,
  onRerun,
}: {
  xml: string
  onRerun: (nodeIds: string[]) => void
}) {
  const run = fromJUnitXml(xml)

  return (
    <TestResults
      run={run}
      title="Running pytest"
      slowThreshold={1000}
      onRerunFailed={(failed) =>
        onRerun(
          failed.map(({ file, test }) =>
            [file, ...(test?.suite ?? []), test?.name].filter(Boolean).join("::")
          )
        )
      }
    />
  )
}

Kendi olaylarınızdan

Herhangi bir runner’dan run-start, test-start, test-end, file-end ve run-end olayları gönderin ve bunları applyTestEvent ile birleştirin. Saf bir fonksiyon olduğu için bir reducer’da, bir store’da ya da sunucuda çalışır.

"use client"

import * as React from "react"

import { TestResults } from "@/components/blocks/test-results/test-results"
import {
  applyTestEvent,
  createTestRun,
  type TestEvent,
  type TestRun,
} from "@/components/blocks/test-results/test-run"

export function LiveRun({ url }: { url: string }) {
  const [run, setRun] = React.useState<TestRun>(() => createTestRun())

  React.useEffect(() => {
    const source = new EventSource(url)
    source.onmessage = (message) => {
      const event = JSON.parse(message.data) as TestEvent
      setRun((current) => applyTestEvent(current, event))
      if (event.type === "run-end") source.close()
    }
    return () => source.close()
  }, [url])

  return <TestResults run={run} />
}

Yapı

Dıştan içe doğru birleştirdiğiniz parçalar.

ParçaAçıklama
TestResultsÇalıştırma: canlı başlık, ilerleme, filtre, eylemler ve dosyalar.
ValueDiffDeğişen kelimeleri işaretlenmiş Expected ve Received.
applyTestEvent, createTestRunAkış hâlindeki olaylardan bir çalıştırma kuran saf yardımcılar.
fromVitestJson, fromJestJson, fromPlaywrightJson, fromJUnitXmlReporter çıktısını bir çalıştırmaya dönüştüren adaptörler.
summarizeRun, formatFailuresÖzet satırı ve bir prompt ya da pano için düz metin olarak hatalar.

TestResults

children dışındaki tüm section prop’larını da kabul eder.

PropTürVarsayılan
runBir adaptörden, applyTestEvent’ten ya da kendi verinizden gelen çalıştırma.
TestRun–
titleTestler çalışırken canlı etiket.
string"Running tests"
labelBölgenin erişilebilir adı.
string"Test results"
slowThresholdEn az bu kadar milisaniye süren testler yavaş sayılır.
number300
rootStack’lerdeki yollardan çıkarılan proje kökü.
string–
defaultOpenÇalıştırmanın açık olup olmadığını geçersiz kılar. Varsayılan olarak çalışırken ve bir şey başarısız olduğunda açık kalır.
boolean–
defaultFilterBaşlangıçtaki filtre.
"all" | "failed" | "skipped" | "slow""all"
onAskToFixAsk to fix’i çalıştırmada ve her hatada gösterir.
(failed: FailedTest[]) => void–
onRerunFailedÇalıştırma hatalarla bittiğinde Rerun failed’ı gösterir.
(failed: FailedTest[]) => void–
onRerunTestHer hatada Rerun’ı gösterir.
(failed: FailedTest) => void–
onUpdateSnapshotSnapshot uyuşmazlıklarında Update snapshot’ı gösterir.
(failed: FailedTest) => void–
getFrameHrefStack frame’lerini, örneğin getEditorHref ile bağlantıya çevirir.
(frame: StackFrame) => string | undefined–
onFrameClickBir frame’e tıklandığında çağrılır.
(frame: StackFrame, event: MouseEvent) => void–
PropTürVarsayılan
statusTestlerin hâlâ çalışıp çalışmadığı.
"running" | "done" | "cancelled"–
filesBaşladıkları sıradaki dosyalar; her biri testleri, süresi ve yüklenemediyse bir hatayla.
TestFile[]–
total“42 of 120” için beklenen test sayısı. Varsayılan olarak dosya başına toplanan sayılar.
number–
durationÖzet için milisaniye. Verilmezse çalışırken ölçülür.
number–
startedAtCanlı zamanlayıcı için çalıştırmanın başladığı an, zaman damgası olarak.
number–
PropTürVarsayılan
nameTestin kendi adı.
string–
suiteOnu saran describe blokları, en dıştaki önce.
string[]–
statusTestin durumu.
"queued" | "running" | "passed" | "failed" | "skipped" | "todo"–
durationMilisaniye.
number–
retriesKaç kez yeniden denendiği.
number–
failureNeden başarısız olduğu.
TestFailure–
PropTürVarsayılan
kind“Timed out after 5s” ya da “Snapshot mismatch” gibi ifadeyi seçer.
"assertion" | "snapshot" | "timeout" | "error"–
messageExpected ve Received satırları olmadan doğrulama mesajı.
string–
expected, receivedKarşılaştırmanın iki tarafı; hatadan, bir diff’ten ya da mesajdan okunur.
string–
stackparseStackTrace için metin. Frame yoksa verilmez.
string–
detailspytest’in hata veren kaynak kodu gibi ek çıktı.
string–
rawCopy’nin kullandığı özgün metin.
string–

applyTestEvent(run, event)

Yeni bir çalıştırma döndürür. createTestRun() başlamak için boş bir tane verir.

PropTürVarsayılan
eventrun-start, file-start, test-start, test-end, file-end ve run-end olaylarından biri. Testler id ile ya da dosya, suite ve ad ile eşleşir.
TestEvent–
event.errorBir string ya da Vitest’in TestError’ı gibi message, stack, expected, actual ve diff içeren bir nesne.
TestErrorInput–

fromVitestJson(report, options)

fromJestJson aynı raporu alır. fromPlaywrightJson, Playwright’ın JSON reporter’ını okur; fromJUnitXml(xml, options) ise JUnit XML’ini okur.

PropTürVarsayılan
options.rootDosya yollarından çıkarılacak proje kökü. Olmadan, yollar src ya da tests gibi ilk klasörden başlar.
string–
TuşAction
TabÇalıştırma başlığı, filtre, eylemler, her dosya ve her başarısız test arasında gezinir.
EnterSpaceÇalıştırmayı, bir dosyayı ya da bir hatayı açar veya kapatır.
←→Filtreler arasında geçiş yapar.
  • Çalıştırma, testler çalışırken aria-busy taşıyan adlandırılmış bir bölgedir. Kibar bir canlı bölge son özeti yalnızca bir kez duyurur; böylece akan testler ve ilerleyen sayaçlar sessiz kalır.
  • Çalıştırma başlığı, her dosya ve her başarısız test aria-expanded özellikli açılır düğmelerdir. Adları özeti, dosyanın sayılarını ya da testin durumunu ve adını okur.
  • Filtre bir toggle grubudur. Ok tuşları seçenekler arasında gezinir ve boş filtreler devre dışıdır.
  • Durum asla yalnızca renkle verilmez: her durumun kendi işareti vardır, ekran okuyucular her testten önce “Passed” ya da “Failed” duyar, Expected ve Received etiketlidir ve değişen kelimeler zorlanmış renklerde altı çizili görünür. Azaltılmış harekette satırlar ve işaretler kaymadan belirir.

Şunlarla geliştirildi

Test Results bloğunun yapıldığı ücretsiz HextaUI bileşenleri. Her biri tek başına yüklenir.

Kod

7 dosya, components/blocks/test-results konumuna eklendi.