diff --git a/resources/checks_snippet.js b/resources/checks_snippet.js
index 0fd65ed5..db512e04 100644
--- a/resources/checks_snippet.js
+++ b/resources/checks_snippet.js
@@ -1,13 +1,26 @@
export function handleSummary(data) {
const checks = []
+
+ function traverseGroup(group) {
+ if (group.checks) {
+ group.checks.forEach((check) => {
+ checks.push(check)
+ })
+ }
+ if (group.groups) {
+ group.groups.forEach((subGroup) => {
+ traverseGroup(subGroup)
+ })
+ }
+ }
+
data.root_group.checks.forEach((check) => {
checks.push(check)
})
data.root_group.groups.forEach((group) => {
- group.checks.forEach((check) => {
- checks.push(check)
- })
+ traverseGroup(group)
})
+
return {
stdout: JSON.stringify(checks),
}
diff --git a/src/preload.ts b/src/preload.ts
index ddd30c50..e4be4c9e 100644
--- a/src/preload.ts
+++ b/src/preload.ts
@@ -1,5 +1,5 @@
import { ipcRenderer, contextBridge, IpcRendererEvent } from 'electron'
-import { ProxyData, K6Log, FolderContent } from './types'
+import { ProxyData, K6Log, FolderContent, K6Check } from './types'
import { HarFile } from './types/har'
import { GeneratorFile } from './types/generator'
import { AddToastPayload } from './types/toast'
@@ -71,6 +71,9 @@ const script = {
onScriptStopped: (callback: () => void) => {
return createListener('script:stopped', callback)
},
+ onScriptCheck: (callback: (data: K6Check[]) => void) => {
+ return createListener('script:check', callback)
+ },
} as const
const har = {
diff --git a/src/views/Validator/CheckRow.tsx b/src/views/Validator/CheckRow.tsx
new file mode 100644
index 00000000..26d39b57
--- /dev/null
+++ b/src/views/Validator/CheckRow.tsx
@@ -0,0 +1,25 @@
+import { K6Check } from '@/types'
+import { Cross2Icon, CheckIcon } from '@radix-ui/react-icons'
+import { Table } from '@radix-ui/themes'
+import { hasFailures, getPassPercentage } from './ChecksSection.utils'
+
+export function CheckRow({ check }: { check: K6Check }) {
+ return (
+
+
+ {hasFailures(check) && (
+
+ )}
+ {!hasFailures(check) && (
+
+ )}
+
+ {check.name}
+
+ {getPassPercentage(check).toFixed(2)}%
+
+ {check.passes}
+ {check.fails}
+
+ )
+}
diff --git a/src/views/Validator/ChecksSection.tsx b/src/views/Validator/ChecksSection.tsx
new file mode 100644
index 00000000..71ccbff2
--- /dev/null
+++ b/src/views/Validator/ChecksSection.tsx
@@ -0,0 +1,82 @@
+import { CollapsibleSection } from '@/components/CollapsibleSection'
+import { K6Check } from '@/types'
+import { css } from '@emotion/react'
+import { InfoCircledIcon } from '@radix-ui/react-icons'
+import { Box, Callout, ScrollArea, Table } from '@radix-ui/themes'
+import { groupChecksByPath } from './ChecksSection.utils'
+import { CheckRow } from './CheckRow'
+
+interface ChecksSectionProps {
+ checks: K6Check[]
+ isRunning: boolean
+}
+
+export function ChecksSection({ checks, isRunning }: ChecksSectionProps) {
+ if (!checks.length || isRunning) {
+ return
+ }
+
+ const groupedChecks = groupChecksByPath(checks)
+
+ return (
+
+
+ {Object.entries(groupedChecks).map(([key, checks]) => (
+
+
+
+
+
+ Name
+
+ Success rate
+
+
+ Success count
+
+
+ Fail count
+
+
+
+
+
+ {checks.map((check) => (
+
+ ))}
+
+
+ >
+ }
+ key={key}
+ defaultOpen
+ >
+
+ {key} ({checks.length})
+
+
+ ))}
+
+
+ )
+}
+
+function NoChecksMessage() {
+ return (
+
+
+
+
+
+ Your checks will appear here.
+
+
+ )
+}
diff --git a/src/views/Validator/ChecksSection.utils.test.ts b/src/views/Validator/ChecksSection.utils.test.ts
new file mode 100644
index 00000000..f183129d
--- /dev/null
+++ b/src/views/Validator/ChecksSection.utils.test.ts
@@ -0,0 +1,76 @@
+import { describe, expect, it } from 'vitest'
+
+import {
+ getPassPercentage,
+ groupChecksByPath,
+ hasFailures,
+} from './ChecksSection.utils'
+import { K6Check } from '@/types'
+
+function buildCheck(data?: Partial) {
+ return {
+ id: self.crypto.randomUUID(),
+ name: 'test check',
+ passes: 0,
+ fails: 0,
+ path: '',
+ ...data,
+ }
+}
+
+describe('Checks Section - utils', () => {
+ describe('getPassPercentage', () => {
+ it('should return the percentage of passes in a check', () => {
+ const check = buildCheck({ passes: 30, fails: 20 })
+ const actual = getPassPercentage(check)
+ const expected = 60
+ expect(actual).toEqual(expected)
+ })
+
+ it('should return 0 if passes and fails are zero', () => {
+ const check = buildCheck({ passes: 0, fails: 0 })
+ const actual = getPassPercentage(check)
+ const expected = 0
+ expect(actual).toEqual(expected)
+ })
+ })
+
+ describe('groupChecksByPath', () => {
+ it('should group checks by path', () => {
+ const check1 = buildCheck({ path: '::check1' })
+ const check2 = buildCheck({ path: '::check2' })
+ const check3 = buildCheck({ path: '::group1::check3' })
+ const check4 = buildCheck({ path: '::group2::check4' })
+ const check5 = buildCheck({ path: '::group2::subgroup::check5' })
+
+ const groupedChecks = groupChecksByPath([
+ check1,
+ check2,
+ check3,
+ check4,
+ check5,
+ ])
+
+ const expected = {
+ default: [check1, check2],
+ group1: [check3],
+ group2: [check4],
+ 'group2::subgroup': [check5],
+ }
+
+ expect(groupedChecks).toMatchObject(expected)
+ })
+ })
+
+ describe('hasFailures', () => {
+ it('should return true when the check has fails', () => {
+ const check = buildCheck({ fails: 1 })
+ expect(hasFailures(check)).toBeTruthy()
+ })
+
+ it('should return false when the check has no fails', () => {
+ const check = buildCheck({ fails: 0 })
+ expect(hasFailures(check)).toBeFalsy()
+ })
+ })
+})
diff --git a/src/views/Validator/ChecksSection.utils.ts b/src/views/Validator/ChecksSection.utils.ts
new file mode 100644
index 00000000..64126213
--- /dev/null
+++ b/src/views/Validator/ChecksSection.utils.ts
@@ -0,0 +1,37 @@
+import { K6Check } from '@/types'
+
+export function groupChecksByPath(checks: K6Check[]) {
+ const result: Record = {
+ default: [],
+ }
+
+ checks.forEach((item) => {
+ const paths = item.path.split('::').filter(Boolean)
+
+ if (paths.length === 1) {
+ result['default']?.push(item)
+ } else {
+ const pathName = paths.slice(0, -1).join('::')
+
+ if (result[pathName]) {
+ result[pathName].push(item)
+ } else {
+ result[pathName] = [item]
+ }
+ }
+ })
+
+ return result
+}
+
+export function hasFailures(check: K6Check) {
+ return check.fails > 0
+}
+
+export function getPassPercentage(check: K6Check) {
+ const total = check.passes + check.fails
+ if (total === 0) {
+ return 0
+ }
+ return (check.passes / total) * 100
+}
diff --git a/src/views/Validator/Validator.tsx b/src/views/Validator/Validator.tsx
index 842cd362..70ee4c93 100644
--- a/src/views/Validator/Validator.tsx
+++ b/src/views/Validator/Validator.tsx
@@ -6,7 +6,7 @@ import { Allotment } from 'allotment'
import { useListenProxyData } from '@/hooks/useListenProxyData'
import { useSetWindowTitle } from '@/hooks/useSetWindowTitle'
-import { K6Log, ProxyData } from '@/types'
+import { K6Check, K6Log, ProxyData } from '@/types'
import { LogsSection } from './LogsSection'
import { ValidatorControls } from './ValidatorControls'
import { View } from '@/components/Layout/View'
@@ -15,6 +15,7 @@ import { ReadOnlyEditor } from '@/components/Monaco/ReadOnlyEditor'
import { getRoutePath } from '@/routeMap'
import { Details } from '@/components/WebLogView/Details'
import { useScriptPath } from './Validator.hooks'
+import { ChecksSection } from './ChecksSection'
export function Validator() {
const [selectedRequest, setSelectedRequest] = useState(null)
@@ -23,6 +24,7 @@ export function Validator() {
const { scriptPath, isExternal } = useScriptPath()
const [isRunning, setIsRunning] = useState(false)
const [logs, setLogs] = useState([])
+ const [checks, setChecks] = useState([])
const navigate = useNavigate()
const { proxyData, resetProxyData } = useListenProxyData()
@@ -87,6 +89,12 @@ export function Validator() {
})
}, [])
+ useEffect(() => {
+ return window.studio.script.onScriptCheck((checks) => {
+ setChecks(checks)
+ })
+ }, [])
+
useEffect(() => {
// Reset requests and logs when script changes
resetProxyData()
@@ -148,6 +156,9 @@ export function Validator() {
Logs ({logs.length})
+
+ Checks ({checks.length})
+
Script
@@ -160,6 +171,15 @@ export function Validator() {
>
+
+
+