Skip to content

Commit

Permalink
feat: display script checks in Validator (#133)
Browse files Browse the repository at this point in the history
  • Loading branch information
cristianoventura authored Sep 10, 2024
1 parent 53915d0 commit dd3be42
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 5 deletions.
19 changes: 16 additions & 3 deletions resources/checks_snippet.js
Original file line number Diff line number Diff line change
@@ -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),
}
Expand Down
5 changes: 4 additions & 1 deletion src/preload.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 = {
Expand Down
25 changes: 25 additions & 0 deletions src/views/Validator/CheckRow.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Table.Row key={check.id}>
<Table.RowHeaderCell>
{hasFailures(check) && (
<Cross2Icon width="18px" height="18px" color="var(--red-11)" />
)}
{!hasFailures(check) && (
<CheckIcon width="18px" height="18px" color="var(--green-11)" />
)}
</Table.RowHeaderCell>
<Table.Cell>{check.name}</Table.Cell>
<Table.Cell align="right">
{getPassPercentage(check).toFixed(2)}%
</Table.Cell>
<Table.Cell align="right">{check.passes}</Table.Cell>
<Table.Cell align="right">{check.fails}</Table.Cell>
</Table.Row>
)
}
82 changes: 82 additions & 0 deletions src/views/Validator/ChecksSection.tsx
Original file line number Diff line number Diff line change
@@ -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 <NoChecksMessage />
}

const groupedChecks = groupChecksByPath(checks)

return (
<ScrollArea scrollbars="vertical">
<Box pb="2">
{Object.entries(groupedChecks).map(([key, checks]) => (
<CollapsibleSection
content={
<>
<Table.Root size="1" variant="surface">
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell></Table.ColumnHeaderCell>
<Table.ColumnHeaderCell>Name</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell align="right">
Success rate
</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell align="right">
Success count
</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell align="right">
Fail count
</Table.ColumnHeaderCell>
</Table.Row>
</Table.Header>

<Table.Body>
{checks.map((check) => (
<CheckRow check={check} key={check.id} />
))}
</Table.Body>
</Table.Root>
</>
}
key={key}
defaultOpen
>
<span
css={css`
font-size: 13px;
font-weight: 500;
`}
>
{key} ({checks.length})
</span>
</CollapsibleSection>
))}
</Box>
</ScrollArea>
)
}

function NoChecksMessage() {
return (
<Box p="2">
<Callout.Root>
<Callout.Icon>
<InfoCircledIcon />
</Callout.Icon>
<Callout.Text>Your checks will appear here.</Callout.Text>
</Callout.Root>
</Box>
)
}
76 changes: 76 additions & 0 deletions src/views/Validator/ChecksSection.utils.test.ts
Original file line number Diff line number Diff line change
@@ -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<K6Check>) {
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()
})
})
})
37 changes: 37 additions & 0 deletions src/views/Validator/ChecksSection.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { K6Check } from '@/types'

export function groupChecksByPath(checks: K6Check[]) {
const result: Record<string, K6Check[]> = {
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
}
22 changes: 21 additions & 1 deletion src/views/Validator/Validator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<ProxyData | null>(null)
Expand All @@ -23,6 +24,7 @@ export function Validator() {
const { scriptPath, isExternal } = useScriptPath()
const [isRunning, setIsRunning] = useState(false)
const [logs, setLogs] = useState<K6Log[]>([])
const [checks, setChecks] = useState<K6Check[]>([])
const navigate = useNavigate()

const { proxyData, resetProxyData } = useListenProxyData()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -148,6 +156,9 @@ export function Validator() {
<Tabs.Trigger value="logs">
Logs ({logs.length})
</Tabs.Trigger>
<Tabs.Trigger value="checks">
Checks ({checks.length})
</Tabs.Trigger>
<Tabs.Trigger value="script">Script</Tabs.Trigger>
</Tabs.List>

Expand All @@ -160,6 +171,15 @@ export function Validator() {
>
<LogsSection logs={logs} autoScroll={isRunning} />
</Tabs.Content>
<Tabs.Content
value="checks"
css={css`
flex: 1;
min-height: 0;
`}
>
<ChecksSection checks={checks} isRunning={isRunning} />
</Tabs.Content>
<Tabs.Content
value="script"
css={css`
Expand Down

0 comments on commit dd3be42

Please sign in to comment.